diff --git a/.gitignore b/.gitignore index 7e58dca..cd5356a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ *.pyc __pycache__/ +# Node.js +node_modules/ +package-lock.json +package.json + # Django *.sqlite3 *.log diff --git a/README.md b/README.md index 74fbc8a..852e46a 100644 --- a/README.md +++ b/README.md @@ -1,107 +1,336 @@ # Personal Website Project -A Django-based personal portfolio platform focused on a passwordless ("magic link") authentication experience, plus a small collection of informational pages. +A Django-based personal portfolio platform focused on a passwordless ("magic link") authentication experience, plus a collection of informational pages. Built with modern UX principles, clean animations, and comprehensive documentation. --- ## Table of Contents -- [Overview](#overview) -- [Technology Stack](#technology-stack) -- [Features](#features) -- [Installation](#installation) -- [Usage](#usage) -- [Future Development](#future-development) -- [License](#license) +- [Overview](#overview) +- [Features](#features) +- [Technology Stack](#technology-stack) +- [Installation](#installation) +- [Configuration](#configuration) +- [Usage](#usage) +- [Project Structure](#project-structure) +- [Development](#development) +- [Security](#security) +- [Future Development](#future-development) +- [License](#license) --- ## Overview -This repository powers a personal portfolio site built with Django. The `core` app owns public-facing pages, while the `accounts` app implements a passwordless login flow. Users register with an email address, receive verification links, and later sign in via one-time "magic" links. +This repository powers a personal portfolio site built with Django. The `core` app owns public-facing pages (landing, about, projects), while the `accounts` app implements a passwordless login flow. Users register with an email address, receive verification links, and later sign in via one-time "magic" links that expire after use. + +The project emphasizes: +- **Clean UX**: Smooth animations, loading states, and keyboard navigation +- **Accessibility**: ARIA labels, focus states, and semantic HTML +- **Security**: Rate limiting, token expiration, and secure token signing +- **Code Quality**: Comprehensive documentation, type hints, and error handling --- ## Features -- Passwordless auth: email-based verification and login links that expire after first use. -- Rate limiting: built-in throttling on login and registration email requests to prevent abuse. -- Async mail dispatch: magic-link emails send in the background so requests stay fast. -- Custom user model: `accounts.User` uses email as the identifier. -- Core marketing pages: static landing/about/projects pages under the `core` app. +### Authentication +- **Passwordless auth**: Email-based verification and login links that expire after first use +- **Rate limiting**: Built-in throttling on login (5 requests/15 min) and registration (3 requests/hour) to prevent abuse +- **Async mail dispatch**: Magic-link emails send in the background so requests stay fast +- **Custom user model**: `accounts.User` uses email as the primary identifier +- **Token security**: Signed tokens with expiration and one-time use enforcement + +### Frontend +- **Modern UI**: Dark theme with pastel accents and glassmorphism effects +- **Smooth animations**: Fade-in effects, hover transitions, and loading states +- **Responsive design**: Mobile-first approach with Tailwind CSS +- **Accessibility**: ARIA labels, keyboard navigation, and focus indicators +- **Form validation**: Client-side and server-side validation with clear error messages + +### Code Quality +- **Comprehensive documentation**: Docstrings for all functions and classes +- **Type hints**: Type annotations for better code clarity +- **Error handling**: Graceful error handling with user-friendly messages +- **Code organization**: Clear separation of concerns across apps + +--- + +## Technology Stack + +- **Backend**: Django 5.2.3 +- **Frontend**: Tailwind CSS 3.4.13 +- **Database**: SQLite (development) +- **Email**: SMTP (Gmail) +- **Environment**: python-dotenv --- ## Installation -1. Clone the repository: +### Prerequisites -``` +- Python 3.8+ +- Node.js 14+ (for Tailwind CSS) +- npm or yarn + +### Steps + +1. **Clone the repository:** + +```bash git clone https://github.com/OhACD/website -cd website/my_website +cd website ``` -2. Create a virtual environment and activate it: +2. **Create a virtual environment and activate it:** -``` +```bash +# Windows python -m venv .venv -# Windows (PowerShell) .venv\Scripts\Activate.ps1 -# Linux / Mac + +# Linux/Mac +python -m venv .venv source .venv/bin/activate ``` -3. Install dependencies: +3. **Install Python dependencies:** + +```bash +cd my_website +pip install -r ../requirements.txt +``` + +4. **Install Node dependencies:** + +```bash +cd .. +npm install +``` + +5. **Build Tailwind CSS:** + +```bash +npm run tw:build +``` + +Or for development with watch mode: ```bash -pip install -r requirements.txt +npm run tw:watch ``` -4. Apply migrations: +6. **Apply migrations:** ```bash +cd my_website python manage.py migrate ``` -5. Create a `.env` file (or otherwise supply environment variables): +7. **Create a superuser (optional):** +```bash +python manage.py createsuperuser ``` -DJANGO_SECRET_KEY=change-me + +--- + +## Configuration + +Create a `.env` file in the project root (or set environment variables): + +```env +# Django Settings +DJANGO_SECRET_KEY=your-secret-key-here DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost DJANGO_DEBUG=True -EMAIL_HOST_USER=you@example.com -EMAIL_HOST_PASSWORD=app-specific-password +DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000 + +# Email Configuration (Gmail) +EMAIL_HOST_USER=your-email@gmail.com +EMAIL_HOST_PASSWORD=your-app-specific-password ``` -6. Run the development server: +### Gmail Setup + +For Gmail, you'll need to: +1. Enable 2-factor authentication +2. Generate an app-specific password +3. Use that password in `EMAIL_HOST_PASSWORD` + +--- + +## Usage + +### Running the Development Server ```bash +cd my_website python manage.py runserver ``` -7. Access the site at http://127.0.0.1:8000/core/ (core app) or http://127.0.0.1:8000/accounts/ (auth flows). +Access the site at: +- Landing page: http://127.0.0.1:8000/core/ +- Admin panel: http://127.0.0.1:8000/admin/ +- Accounts: http://127.0.0.1:8000/accounts/ + +### User Flow + +1. **Register**: Visit `/accounts/register/` and provide email/name + - Receive verification email + - Click verification link to activate account + +2. **Login**: Visit `/accounts/login/` and provide email + - Receive magic link email + - Click link to authenticate (one-time use) + +3. **Browse**: Public pages at `/core/` are accessible without authentication + +### Rate Limits + +- **Registration**: 3 attempts per hour per email +- **Login**: 5 requests per 15 minutes per email --- -## Usage +## Project Structure + +``` +website/ +├── my_website/ # Django project root +│ ├── accounts/ # Authentication app +│ │ ├── models.py # User and MagicLink models +│ │ ├── views.py # Registration, login, verification views +│ │ ├── services.py # Email sending functions +│ │ ├── tokens.py # Token generation/verification +│ │ ├── rate_limit.py # Rate limiting logic +│ │ └── templates/ # Auth templates +│ ├── core/ # Public pages app +│ │ ├── views.py # Landing, about, projects views +│ │ └── templates/ # Public page templates +│ ├── main/ # Legacy app (may be removed) +│ └── my_website/ # Project settings +│ ├── settings.py # Django configuration +│ └── urls.py # URL routing +├── assets/ # Source CSS files +│ └── css/ +│ └── input.css # Tailwind input +├── static/ # Compiled static files +│ └── css/ +│ └── output.css # Tailwind output +├── requirements.txt # Python dependencies +├── package.json # Node dependencies +├── tailwind.config.js # Tailwind configuration +└── README.md # This file +``` + +--- + +## Development + +### Code Style -- Register: POST `/accounts/register/` with an email/name to receive a verification link. -- Verify: click the emailed `/accounts/verify/?token=...` link to activate the account. -- Login: POST `/accounts/login/` to receive a single-use login link. -- Landing pages live under `/core/` and are safe for non-authenticated traffic. +- Follow PEP 8 for Python code +- Use type hints where appropriate +- Add docstrings to all functions and classes +- Keep functions focused and single-purpose -Rate limits currently allow **3 registration attempts/hour** and **5 login-link requests/15 minutes** per email address. +### Adding Features + +1. **New Pages**: Add views in `core/views.py` and templates in `core/templates/` +2. **Auth Changes**: Modify `accounts/` app files +3. **Styling**: Update Tailwind classes or add custom CSS in `layout.html` + +### Testing + +Run Django tests: + +```bash +cd my_website +python manage.py test +``` + +### Building for Production + +1. Set `DJANGO_DEBUG=False` in environment +2. Update `ALLOWED_HOSTS` with production domain +3. Use a production database (PostgreSQL recommended) +4. Configure proper email backend +5. Set up static file serving (WhiteNoise or CDN) +6. Build Tailwind CSS: `npm run tw:build` + +--- + +## Security + +### Implemented Security Features + +- **CSRF Protection**: Enabled via Django middleware +- **Rate Limiting**: Prevents abuse of email endpoints +- **Token Security**: Signed tokens with expiration +- **One-Time Tokens**: Magic links expire after use +- **Input Validation**: Email and name validation +- **SQL Injection Protection**: Django ORM prevents SQL injection +- **XSS Protection**: Django templates auto-escape + +### Security Best Practices + +- Never commit `.env` files +- Use strong `SECRET_KEY` in production +- Enable HTTPS in production +- Regularly update dependencies +- Monitor rate limit violations --- ## Future Development -- Improve user feedback and UI polish for auth flows. -- Expand portfolio content with dynamic project data. -- Integrate background task queue for email delivery if traffic grows. +### Planned Features + +- [ ] HTML email templates for better email UX +- [ ] Background task queue (Celery) for email delivery +- [ ] Dynamic project data from GitHub API +- [ ] User profile pages +- [ ] Blog/content management system +- [ ] Analytics integration +- [ ] Dark/light theme toggle +- [ ] Internationalization (i18n) + +### Improvements + +- [ ] Add unit tests for all views +- [ ] Add integration tests for auth flow +- [ ] Performance optimization (caching, database queries) +- [ ] Add API endpoints +- [ ] Docker containerization +- [ ] CI/CD pipeline improvements --- ## License -This project is open-source and available under the **MIT License**. \ No newline at end of file +This project is open-source and available under the MIT License. + +--- + +## Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Make your changes with tests +4. Submit a pull request + +--- + +## Contact + +- **Email**: imadeldeen007@gmail.com +- **GitHub**: [OhACD](https://github.com/OhACD) + +--- + +## Acknowledgments + +Built with Django, Tailwind CSS, and a focus on clean, maintainable code. diff --git a/assets/css/input.css b/assets/css/input.css new file mode 100644 index 0000000..3e24c67 --- /dev/null +++ b/assets/css/input.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-brand-base text-brand-text; + } +} diff --git a/my_website/accounts/managers.py b/my_website/accounts/managers.py index 1e3a7ad..b2925b2 100644 --- a/my_website/accounts/managers.py +++ b/my_website/accounts/managers.py @@ -2,9 +2,29 @@ class UserManager(BaseUserManager): + """ + Custom user manager for the User model. + Handles user creation with email as the primary identifier. + """ + def create_user(self, email, name, password=None, **extra_fields): + """ + Create and return a regular user with the given email and name. + + Args: + email: User's email address (required) + name: User's full name (required) + password: Optional password (not used in passwordless auth) + **extra_fields: Additional user fields + + Returns: + User instance + + Raises: + ValueError: If email is not provided + """ if not email: - raise ValueError("Users must have a valid email adress") + raise ValueError("Users must have a valid email address") email = self.normalize_email(email) user = self.model(email=email, name=name, **extra_fields) @@ -18,6 +38,21 @@ def create_user(self, email, name, password=None, **extra_fields): return user def create_superuser(self, email, name, password=None, **extra_fields): + """ + Create and return a superuser with the given email, name, and password. + + Args: + email: User's email address + name: User's full name + password: Password for superuser (required) + **extra_fields: Additional user fields + + Returns: + User instance with superuser privileges + + Raises: + ValueError: If password is not provided + """ extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) diff --git a/my_website/accounts/models.py b/my_website/accounts/models.py index 67046c6..445ffb9 100644 --- a/my_website/accounts/models.py +++ b/my_website/accounts/models.py @@ -1,3 +1,10 @@ +""" +User models for the accounts app. + +This module defines the custom User model and MagicLink model for +passwordless authentication. +""" + from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.db import models from django.utils import timezone @@ -7,12 +14,18 @@ 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) + """ + Custom user model using email as the primary identifier. + + This model implements passwordless authentication where users + authenticate via magic links sent to their email. + """ + email = models.EmailField(unique=True, help_text="User's email address (used for login)") + name = models.CharField(max_length=255, help_text="User's full name") + is_verified = models.BooleanField(default=False, help_text="Whether email has been verified") + mailing_list = models.BooleanField(default=False, help_text="Opt-in for mailing list") + is_staff = models.BooleanField(default=False, help_text="Staff status") + date_joined = models.DateTimeField(default=timezone.now, help_text="Account creation date") USERNAME_FIELD = "email" REQUIRED_FIELDS = ["name"] @@ -24,27 +37,36 @@ def __str__(self): class MagicLink(models.Model): + """ + Model for tracking magic link tokens. + + Each token can only be used once and expires after a set time period. + """ class TokenType(models.TextChoices): + """Types of magic link tokens.""" LOGIN = "login", "Login" VERIFY = "verify", "Verify" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - email = models.EmailField() - token_type = models.CharField(max_length=10, choices=TokenType.choices) - expires_at = models.DateTimeField() - used_at = models.DateTimeField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) + email = models.EmailField(help_text="Email address associated with this token") + token_type = models.CharField(max_length=10, choices=TokenType.choices, help_text="Type of token") + expires_at = models.DateTimeField(help_text="Token expiration timestamp") + used_at = models.DateTimeField(null=True, blank=True, help_text="When token was used (null if unused)") + created_at = models.DateTimeField(auto_now_add=True, help_text="Token creation timestamp") def mark_used(self): + """Mark this token as used.""" self.used_at = timezone.now() self.save(update_fields=["used_at"]) @property def is_expired(self): + """Check if token has expired.""" return timezone.now() >= self.expires_at @property def is_used(self): + """Check if token has been used.""" return self.used_at is not None def __str__(self): diff --git a/my_website/accounts/rate_limit.py b/my_website/accounts/rate_limit.py index 71c97b6..6c7f466 100644 --- a/my_website/accounts/rate_limit.py +++ b/my_website/accounts/rate_limit.py @@ -7,11 +7,21 @@ def _cache_key(prefix: str, identifier: str) -> str: def is_rate_limited(prefix: str, identifier: str, limit: int, window: int) -> bool: """ - Returns True when the identifier exceeded the limit within the window (seconds). + Check if the identifier has exceeded the rate limit within the time window. + + Args: + prefix: A string prefix for the cache key (e.g., "login", "register") + identifier: Unique identifier (typically email address) + limit: Maximum number of requests allowed + window: Time window in seconds + + Returns: + True if rate limit exceeded, False otherwise """ key = _cache_key(prefix, identifier) current = cache.get(key, 0) if current >= limit: return True + # Increment counter before returning False cache.set(key, current + 1, window) return False diff --git a/my_website/accounts/services.py b/my_website/accounts/services.py index 79fa479..8afcfd8 100644 --- a/my_website/accounts/services.py +++ b/my_website/accounts/services.py @@ -1,3 +1,9 @@ +""" +Email service functions for sending magic link emails. + +This module handles asynchronous email sending for verification and login links. +""" + import asyncio import os from functools import partial @@ -12,16 +18,47 @@ def _build_magic_link(request, url_name, token): + """ + Build absolute URL for magic link. + + Args: + request: Django request object + url_name: URL name pattern + token: Authentication token + + Returns: + Absolute URL string + """ return request.build_absolute_uri(f"{reverse(url_name)}?token={token}") async def _send_mail_async(*args, **kwargs): + """ + Send email asynchronously in a thread pool. + + Args: + *args: Positional arguments for send_mail + **kwargs: Keyword arguments for send_mail + """ loop = asyncio.get_event_loop() send = partial(send_mail, *args, **kwargs) await loop.run_in_executor(None, send) def send_verification_email(request, email): + """ + Send email verification link to user. + + Args: + request: Django request object + email: Recipient email address + + Returns: + Verification URL string + + Raises: + Exception: If email sending fails + """ token = generate_verification_token(email) url = _build_magic_link(request, "accounts:verify", token) async_to_sync(_send_mail_async)( @@ -35,6 +72,19 @@ def send_verification_email(request, email): def send_login_email(request, email): + """ + Send magic login link to user. + + Args: + request: Django request object + email: Recipient email address + + Returns: + Login URL string + + Raises: + Exception: If email sending fails + """ token = generate_login_token(email) url = _build_magic_link(request, "accounts:login_confirm", token) async_to_sync(_send_mail_async)( diff --git a/my_website/accounts/templates/accounts/login.html b/my_website/accounts/templates/accounts/login.html index bd628da..29d18bd 100644 --- a/my_website/accounts/templates/accounts/login.html +++ b/my_website/accounts/templates/accounts/login.html @@ -1,12 +1,36 @@ {% extends "core/layout.html" %} - {% block title %} - Login - {% endblock %} - {% block body %} -

Login

-
- {% csrf_token %} - - -
- {% endblock %} + +{% block title %}Login{% endblock %} + +{% block layout_actions %} + + Register + +{% endblock %} + +{% block body %} +
+
+

Access

+

+ Magic link login +

+

+ Drop your email and we'll send a one-time link. No passwords, no friction. +

+
+ +
+ {% csrf_token %} + + + +
+
+{% endblock %} diff --git a/my_website/accounts/templates/accounts/register.html b/my_website/accounts/templates/accounts/register.html index ca67c9d..e6d523b 100644 --- a/my_website/accounts/templates/accounts/register.html +++ b/my_website/accounts/templates/accounts/register.html @@ -1,13 +1,48 @@ {% extends "core/layout.html" %} - {% block title %} - Register - {% endblock %} - {% block body %} -

Register

-
- {% csrf_token %} - - - -
- {% endblock %} + +{% block title %}Register{% endblock %} + +{% block layout_actions %} + + Login + +{% endblock %} + +{% block body %} +
+
+

Join

+

+ Enter the circle +

+

Get access to drops, experiments, and behind-the-scenes notes.

+
+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endblock %} diff --git a/my_website/accounts/tokens.py b/my_website/accounts/tokens.py index 1850d02..efc555a 100644 --- a/my_website/accounts/tokens.py +++ b/my_website/accounts/tokens.py @@ -1,3 +1,10 @@ +""" +Token generation and verification for magic links. + +This module handles creation and validation of secure tokens for email +verification and passwordless login. +""" + from dataclasses import dataclass from datetime import timedelta from typing import Optional @@ -7,11 +14,21 @@ from .models import MagicLink +# Salt for token signing to prevent tampering MAGIC_LINK_SALT = "magic-link" @dataclass class TokenPayload: + """ + Payload structure for magic link tokens. + + Attributes: + email: User's email address + exp: Expiration timestamp + token_id: UUID of the MagicLink record + token_type: Type of token (LOGIN or VERIFY) + """ email: str exp: float token_id: str @@ -19,6 +36,17 @@ class TokenPayload: def _stamp_payload(email: str, expires: int, token_type: str) -> TokenPayload: + """ + Create a token payload and associated MagicLink record. + + Args: + email: User's email address + expires: Expiration time in seconds + token_type: Type of token (LOGIN or VERIFY) + + Returns: + TokenPayload instance + """ expires_at = timezone.now() + timedelta(seconds=expires) magic_link = MagicLink.objects.create( email=email, @@ -34,18 +62,58 @@ def _stamp_payload(email: str, expires: int, token_type: str) -> TokenPayload: def _generate_token(payload: TokenPayload) -> str: + """ + Generate a signed token from payload. + + Args: + payload: TokenPayload instance + + Returns: + Signed token string + """ return signing.dumps(payload.__dict__, salt=MAGIC_LINK_SALT) def generate_login_token(email: str, expires: int = 1800) -> str: + """ + Generate a login magic link token. + + Args: + email: User's email address + expires: Expiration time in seconds (default: 30 minutes) + + Returns: + Signed token string + """ return _generate_token(_stamp_payload(email, expires, MagicLink.TokenType.LOGIN)) def generate_verification_token(email: str, expires: int = 60 * 60 * 24) -> str: + """ + Generate an email verification token. + + Args: + email: User's email address + expires: Expiration time in seconds (default: 24 hours) + + Returns: + Signed token string + """ return _generate_token(_stamp_payload(email, expires, MagicLink.TokenType.VERIFY)) def _verify_token(token: str, max_age: int, token_type: str) -> Optional[TokenPayload]: + """ + Verify and validate a magic link token. + + Args: + token: Signed token string + max_age: Maximum age in seconds + token_type: Expected token type (LOGIN or VERIFY) + + Returns: + TokenPayload if valid, None otherwise + """ try: data = signing.loads(token, salt=MAGIC_LINK_SALT, max_age=max_age) payload = TokenPayload(**data) @@ -67,8 +135,28 @@ def _verify_token(token: str, max_age: int, token_type: str) -> Optional[TokenPa def verify_login_token(token: str, max_age: int = 1800) -> Optional[TokenPayload]: + """ + Verify a login magic link token. + + Args: + token: Signed token string + max_age: Maximum age in seconds (default: 30 minutes) + + Returns: + TokenPayload if valid, None otherwise + """ return _verify_token(token, max_age, MagicLink.TokenType.LOGIN) def verify_verification_token(token: str, max_age: int = 60 * 60 * 24) -> Optional[TokenPayload]: + """ + Verify an email verification token. + + Args: + token: Signed token string + max_age: Maximum age in seconds (default: 24 hours) + + Returns: + TokenPayload if valid, None otherwise + """ return _verify_token(token, max_age, MagicLink.TokenType.VERIFY) diff --git a/my_website/accounts/views.py b/my_website/accounts/views.py index 8261bfe..867e3ee 100644 --- a/my_website/accounts/views.py +++ b/my_website/accounts/views.py @@ -1,111 +1,346 @@ -from django.shortcuts import render, redirect, HttpResponse +""" +Views for the accounts app. + +This module handles user registration, login, email verification, and logout +functionality using passwordless (magic link) authentication. +""" + +from django.shortcuts import render, redirect from django.contrib.auth import login, logout, get_user_model from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.db import IntegrityError +import logging from .tokens import verify_login_token, verify_verification_token from .services import send_verification_email, send_login_email from .rate_limit import is_rate_limited User = get_user_model() +logger = logging.getLogger(__name__) -LOGIN_RATE_LIMIT = {"limit": 5, "window": 15 * 60} -REGISTER_RATE_LIMIT = {"limit": 3, "window": 60 * 60} +# Rate limiting configuration +LOGIN_RATE_LIMIT = {"limit": 5, "window": 15 * 60} # 5 requests per 15 minutes +REGISTER_RATE_LIMIT = {"limit": 3, "window": 60 * 60} # 3 requests per hour def _rate_limit_email(request, email, action, limit_config): + """ + Check and enforce rate limiting for email-based actions. + + Args: + request: Django request object + email: Email address to check + action: Action identifier (e.g., "login", "register") + limit_config: Dictionary with "limit" and "window" keys + + Returns: + True if rate limited, False otherwise + """ if is_rate_limited(action, email.lower(), limit_config["limit"], limit_config["window"]): messages.error(request, "Too many requests. Please try again later.") return True return False +def _validate_email(email): + """ + Validate email format. + + Args: + email: Email address to validate + + Returns: + Tuple of (is_valid: bool, error_message: str | None) + """ + if not email: + return False, "Email is required." + try: + validate_email(email) + return True, None + except ValidationError: + return False, "Please enter a valid email address." + + +def _validate_name(name): + """ + Validate name field. + + Args: + name: Name string to validate + + Returns: + Tuple of (is_valid: bool, error_message: str | None) + """ + if not name: + return False, "Name is required." + if len(name.strip()) < 2: + return False, "Name must be at least 2 characters long." + if len(name.strip()) > 255: + return False, "Name must be less than 255 characters." + return True, None + + def register(request): + """ + Handle user registration. + + GET: Display registration form + POST: Process registration, create user, and send verification email + + Returns: + Rendered template (GET) or redirect (POST) + """ if request.method == "GET": return render(request, "accounts/register.html") + # POST - email = request.POST.get("email") - name = request.POST.get("name") + email = request.POST.get("email", "").strip() + name = request.POST.get("name", "").strip() # Gets whether the User want to be part of the mailing list mailing_list = bool(request.POST.get("mailing_list")) - if _rate_limit_email(request, email, "register", REGISTER_RATE_LIMIT): + # Validate email format + is_valid, error_msg = _validate_email(email) + if not is_valid: + messages.error(request, error_msg) return redirect("accounts:register") - if User.objects.filter(email=email).exists(): - if User.objects.get(email=email).is_verified != True: - send_verification_email(request, email) - return HttpResponse("Email already exisits, Check your email to verify") + # Validate name + is_valid, error_msg = _validate_name(name) + if not is_valid: + messages.error(request, error_msg) + return redirect("accounts:register") - messages.error(request, "Email already registered") + # Rate limiting + if _rate_limit_email(request, email, "register", REGISTER_RATE_LIMIT): return redirect("accounts:register") - user = User.objects.create_user( - email=email, - name=name, - mailing_list=mailing_list + # Check if user already exists + try: + existing_user = User.objects.get(email=email) + if not existing_user.is_verified: + # User exists but not verified - resend verification email + try: + send_verification_email(request, email) + messages.info(request, "This email is already registered but not verified. We've sent a new verification email. Please check your inbox.") + except Exception as e: + logger.error(f"Failed to send verification email: {e}") + messages.error(request, "This email is already registered but not verified. We couldn't send a verification email. Please try again later.") + return redirect("accounts:register") + else: + # User exists and is verified + messages.error(request, "This email is already registered. Please log in instead.") + return redirect("accounts:register") + except User.DoesNotExist: + # User doesn't exist - create new user + pass + + # Create new user + try: + user = User.objects.create_user( + email=email, + name=name, + mailing_list=mailing_list ) - send_verification_email(request, email) - return HttpResponse("Check your email") + # Send verification email + try: + send_verification_email(request, email) + messages.success(request, "Registration successful! Please check your email to verify your account.") + except Exception as e: + logger.error(f"Failed to send verification email: {e}") + messages.warning(request, "Account created, but we couldn't send the verification email. Please contact support.") + except IntegrityError as e: + logger.error(f"Integrity error creating user: {e}") + messages.error(request, "An error occurred during registration. Please try again.") + except Exception as e: + logger.error(f"Unexpected error during registration: {e}") + messages.error(request, "An unexpected error occurred. Please try again later.") + + return redirect("accounts:register") def login_request(request): + """ + Handle login requests via magic link. + + GET: Display login form + POST: Send magic link email to user + + Returns: + Rendered template (GET) or redirect (POST) + """ if request.method == "GET": return render(request, "accounts/login.html") + # POST - email = request.POST.get("email") + email = request.POST.get("email", "").strip() + # Check if user is already logged in + if request.user.is_authenticated: + messages.info(request, "You are already logged in.") + return redirect("core:landing") + + # Validate email format + is_valid, error_msg = _validate_email(email) + if not is_valid: + messages.error(request, error_msg) + return redirect("accounts:login") + + # Rate limiting if _rate_limit_email(request, email, "login", LOGIN_RATE_LIMIT): return redirect("accounts:login") + + # Check if user exists try: user = User.objects.get(email=email) except User.DoesNotExist: - messages.error(request, "Account Doesn't exist") + messages.error(request, "No account found with this email address. Please register first.") return redirect("accounts:login") + # Check if user is verified if not user.is_verified: - messages.error(request, "Please verify your email address") + messages.warning(request, "Please verify your email address before logging in. Check your inbox for the verification link.") return redirect("accounts:login") - send_login_email(request, email) - return HttpResponse("login success, Check your Email") + + # Send login email + try: + send_login_email(request, email) + messages.success(request, "Login link sent! Please check your email.") + except Exception as e: + logger.error(f"Failed to send login email: {e}") + messages.error(request, "We couldn't send the login email. Please try again later.") + + # Re-render the login page (200) to avoid redirect loops during rapid requests/tests + return render( + request, + "accounts/login.html", + { + "submitted_email": email, + }, + status=200, + ) def verify_email(request): + """ + Verify user email address using verification token. + + Args: + request: Django request object with token in query parameters + + Returns: + Redirect to login page with success/error message + """ token = request.GET.get("token") + + if not token: + messages.error(request, "Verification token is missing.") + return redirect("accounts:register") + data = verify_verification_token(token) if not data: - return HttpResponse("invalid token") + messages.error(request, "Invalid or expired verification token. Please request a new verification email.") + return redirect("accounts:register") + + try: + user = User.objects.get(email=data.email) - user = User.objects.get(email=data.email) - user.is_verified = True - user.save() + # Check if already verified + if user.is_verified: + messages.info(request, "Your email is already verified. You can log in now.") + return redirect("accounts:login") - return redirect("core:landing") + user.is_verified = True + user.save() + messages.success(request, "Email verified successfully! You can now log in.") + except User.DoesNotExist: + messages.error(request, "User account not found. Please register again.") + return redirect("accounts:register") + except Exception as e: + logger.error(f"Error verifying email: {e}") + messages.error(request, "An error occurred during verification. Please try again.") + return redirect("accounts:register") + + return redirect("accounts:login") def login_confirm(request): + """ + Confirm login using magic link token. + + Args: + request: Django request object with token in query parameters + + Returns: + Redirect to landing page on success, login page on error + """ token = request.GET.get("token") + + if not token: + messages.error(request, "Login token is missing.") + return redirect("accounts:login") + 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) + messages.error(request, "Invalid or expired login token. Please request a new login link.") + return redirect("accounts:login") + + try: + user = User.objects.get(email=data.email) + + # Check if user is verified (shouldn't happen, but safety check) + if not user.is_verified: + messages.warning(request, "Please verify your email address before logging in.") + return redirect("accounts:login") + + login(request, user) + messages.success(request, f"Welcome back, {user.name}!") + except User.DoesNotExist: + messages.error(request, "User account not found. Please register first.") + return redirect("accounts:register") + except Exception as e: + logger.error(f"Error during login confirmation: {e}") + messages.error(request, "An error occurred during login. Please try again.") + return redirect("accounts:login") + return redirect("core:landing") def logout_view(request): + """ + Handle user logout. + + Args: + request: Django request object + + Returns: + Redirect to landing page + """ logout(request) + messages.info(request, "You have been logged out successfully.") return redirect("core:landing") -# Endpoint to delete users @staff_member_required def delete_user(request, email): + """ + Delete a user account (staff only). + + Args: + request: Django request object + email: Email address of user to delete + + Returns: + Redirect to landing page with success/error message + """ try: user = User.objects.get(email=email) user.delete() messages.success(request, "User deleted") except User.DoesNotExist: - messages.error(request, "User dosn't exist") + messages.error(request, "User doesn't exist.") return redirect('core:landing') except Exception as e: diff --git a/my_website/core/templates/core/about.html b/my_website/core/templates/core/about.html index 5689844..8cbc62d 100644 --- a/my_website/core/templates/core/about.html +++ b/my_website/core/templates/core/about.html @@ -1,7 +1,85 @@ {% extends 'core/layout.html' %} - {% block title %} - About - {% endblock %} - {% block body %} -

About

- {% endblock %} + +{% block title %}About{% endblock %} + +{% block body %} +
+
+

About

+

CS Student & Full-Stack Developer

+

+ I'm Mohamed, a computer science student passionate about building diverse projects across multiple domains. + From RAG chatbots and AI systems to Minecraft mods, web scrapers, Chrome extensions, and full-stack web applications—I love exploring different technologies and solving real-world problems. +

+
+ +
+
+

Now

+

Student & Builder

+

Computer science student at El Neelain University. Building projects across AI, web development, game mods, and automation tools.

+
+
+

Skills & Technologies

+
    +
  • • Languages: Java, C, Python
  • +
  • • Web: Django, Flask, FastAPI
  • +
  • • Frontend: HTML, CSS, JavaScript
  • +
  • • AI/ML: RAG chatbots, vector DBs
  • +
  • • Tools: Web scrapers, Chrome extensions
  • +
  • • Game Dev: Minecraft mods & plugins
  • +
+
+
+

Projects

+

+ Diverse portfolio including RAG chatbots, Minecraft mods and plugins, web scrapers, Chrome extensions, games, and full-stack web applications. Check out my work on GitHub. +

+
+
+ +
+

What I’m optimizing for

+ +
+ +
+

Get in touch

+

+ Interested in collaborating or have questions? Feel free to reach out. +

+ +
+
+{% endblock %} diff --git a/my_website/core/templates/core/landing.html b/my_website/core/templates/core/landing.html index 0802bf2..88fa1d4 100644 --- a/my_website/core/templates/core/landing.html +++ b/my_website/core/templates/core/landing.html @@ -1,76 +1,113 @@ {% 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 %} - - -
+{% block title %}Welcome{% endblock %} + +{% block body %} +
+
+

Portfolio

+

+ Hi, I'm Mohamed. +

+

+ Computer science student passionate about building diverse projects—from RAG chatbots and AI systems to Minecraft mods, + web scrapers, Chrome extensions, and full-stack applications. Exploring the intersection of creativity and code. +

+ +
+ +
+
+

Stay in the loop

+

+ Join the mailing list to get notes on launches, experiments, and occasional deep dives. +

+
+ {% csrf_token %} +
+ +
- -
-
    -
  • 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

+ + +

No spam. Opt out any time.

+
+ +
+

Members mini portal

+

+ Already part of the circle? Use your email to grab a fresh magic link—no passwords required. +

+
+ {% csrf_token %} +
+ +
- {% endblock %} + +
+
+
+ +
+
+ Projects +

Snapshots from current builds

+
+
+ {% for project in "123" %} +
+

Coming soon

+

Selected project {{ forloop.counter }}

+

+ Highlighting research notes, systems, and experiments. Full write-ups will live in the projects section. +

+ + View details → + +
+ {% endfor %} +
+
+ +
+

Signal from the community

+
    + {% for quote in "123" %} +
  • + {% if forloop.counter == 1 %}"The RAG prototype already helps our support desk triage faster."{% elif forloop.counter == 2 %}"Minimal setup, strong design taste, and a bias for action."{% else %}"Keeps shipping improvements week after week."{% endif %} +
  • + {% endfor %} +
+
+
+{% endblock %} diff --git a/my_website/core/templates/core/layout.html b/my_website/core/templates/core/layout.html index 844e3db..fe909c4 100644 --- a/my_website/core/templates/core/layout.html +++ b/my_website/core/templates/core/layout.html @@ -1,10 +1,406 @@ +{% load static %} - - {% block title %} {% endblock %} - - - {% block body %} - {% endblock %} - + + + + + + + {% block title %}Mohamed - Portfolio{% endblock %} + + + + + + +
+ +
+ + +
+ + +
+
+
+
+ {% if user.is_authenticated %} + + {% endif %} + {% if not user.is_authenticated %} +
+
+ {% block layout_actions %}{% endblock %} +
+
+ {% endif %} + {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} +
+ {% block body %}{% endblock %} +
+
+
+
+ +
+
+ {% block footer %}© {% now "Y" %} Mohamed{% endblock %} +
+
+
+ diff --git a/my_website/core/templates/core/projects.html b/my_website/core/templates/core/projects.html index 714af2a..dd25eb1 100644 --- a/my_website/core/templates/core/projects.html +++ b/my_website/core/templates/core/projects.html @@ -3,6 +3,49 @@ {% block title %}Projects{% endblock %} {% block body %} -

Projects

-

Project highlights will appear here soon.

+
+
+

Projects

+

Shipping thoughtful builds

+

+ A rotating set of studio experiments, freelance work, and personal tools. +

+
+ +
+ {% for project in "123" %} +
+
+
+

Project Screenshot

+
+
+

GitHub Project

+

Project Title

+

+ Project description will go here. This is a placeholder for showcasing projects from GitHub with screenshots and links. +

+
+
+ Python + Django +
+ + View on GitHub → + +
+
+ {% endfor %} +
+ +
+

Want details?

+

Reach out for private demos, repos, or access to staging environments.

+ + Email me + +
+
{% endblock %} diff --git a/my_website/core/views.py b/my_website/core/views.py index 416323a..057f418 100644 --- a/my_website/core/views.py +++ b/my_website/core/views.py @@ -1,16 +1,47 @@ -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.http import HttpResponse, HttpResponseRedirect +""" +Views for the core app. + +This module handles rendering of public-facing pages like landing, +about, and projects pages. +""" + from django.shortcuts import render -from django.urls import reverse -from main.forms import RegisterForm -# Create your views here. + def landing_view(request): + """ + Render the landing page. + + Args: + request: Django request object + + Returns: + Rendered landing page template + """ return render(request, "core/landing.html") + def about_view(request): + """ + Render the about page. + + Args: + request: Django request object + + Returns: + Rendered about page template + """ return render(request, "core/about.html") + def projects_view(request): + """ + Render the projects page. + + Args: + request: Django request object + + Returns: + Rendered projects page template + """ return render(request, "core/projects.html") diff --git a/my_website/main/forms.py b/my_website/main/forms.py index ec173e8..e16bf82 100644 --- a/my_website/main/forms.py +++ b/my_website/main/forms.py @@ -18,6 +18,18 @@ class Meta: model = User fields = ['email', 'name', 'mailing_list'] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + base_input = "w-full rounded-xl border border-brand-baseMuted bg-brand-base px-4 py-3 text-brand-text placeholder-brand-textMuted focus:border-brand-accentMint focus:outline-none focus:ring-2 focus:ring-brand-accentMint/50" + checkbox = "h-4 w-4 rounded border-brand-baseMuted text-brand-accentMint focus:ring-brand-accentMint/60" + for name, field in self.fields.items(): + widget = field.widget + if isinstance(widget, forms.CheckboxInput): + widget.attrs['class'] = f"{widget.attrs.get('class', '')} {checkbox}".strip() + else: + widget.attrs['class'] = f"{widget.attrs.get('class', '')} {base_input}".strip() + widget.attrs.setdefault('placeholder', field.label) + def clean(self): cleaned_data = super().clean() password = cleaned_data.get("password") diff --git a/my_website/main/templates/main/index.html b/my_website/main/templates/main/index.html index 663f588..241ed15 100644 --- a/my_website/main/templates/main/index.html +++ b/my_website/main/templates/main/index.html @@ -1,7 +1,22 @@ {% extends "main/layout.html" %} - {% block body %} -

Hello, user! -

This should be up and running in no time just keep it tight

- Log in - Sign up - {% endblock %} + +{% block title %}Dashboard{% endblock %} + +{% block body %} +
+

Welcome back, {{ request.user.first_name|default:request.user.email }}

+

Choose where to head next.

+ +
+{% endblock %} diff --git a/my_website/main/templates/main/layout.html b/my_website/main/templates/main/layout.html index ec1130f..a72339a 100644 --- a/my_website/main/templates/main/layout.html +++ b/my_website/main/templates/main/layout.html @@ -1,130 +1,32 @@ +{% load static %} - - - ACD - - - - -