add register, login, logout views

This commit is contained in:
Matteo Rosati
2026-02-11 11:53:06 +01:00
parent 160a8bf020
commit 6bce72e866
13 changed files with 402 additions and 3 deletions

Binary file not shown.

View File

@@ -2,4 +2,7 @@ from django.apps import AppConfig
class FrontendConfig(AppConfig): class FrontendConfig(AppConfig):
name = 'frontend' name = "frontend"
def ready(self) -> None:
from . import models # noqa: F401

View File

@@ -0,0 +1,37 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="UserProfile",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("display_name", models.CharField(max_length=150)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@@ -1,3 +1,25 @@
from django.conf import settings
from django.db import models from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
# Create your models here.
class UserProfile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
display_name = models.CharField(max_length=150)
def __str__(self):
return f"{self.display_name} ({self.user.username})" # type: ignore[attr-defined]
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_for_user(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create( # type: ignore[attr-defined]
user=instance,
display_name=instance.username,
)

View File

@@ -2,3 +2,120 @@
width: 100%; width: 100%;
height: 100vh; height: 100vh;
} }
.auth-shell {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 1.5rem;
background: radial-gradient(circle at top, #eef5ff, #f7f0ff 35%, #f7f6ff 65%, #fdf6ef);
color: #1f1f1f;
font-family: "JetBrains Mono", "SFMono-Regular", "Menlo", "Monaco", "Consolas", monospace;
}
.auth-card {
width: min(420px, 100%);
background: #ffffff;
border-radius: 24px;
padding: 2.5rem;
box-shadow: 0 24px 60px rgba(24, 31, 59, 0.12);
border: 1px solid rgba(0, 0, 0, 0.05);
}
.auth-header {
margin-bottom: 2rem;
}
.auth-kicker {
text-transform: uppercase;
letter-spacing: 0.2em;
font-size: 0.7rem;
color: #57606a;
margin-bottom: 0.75rem;
}
.auth-title {
font-size: 1.6rem;
margin-bottom: 0.5rem;
color: #111827;
}
.auth-subtitle {
font-size: 0.95rem;
color: #6b7280;
}
.auth-messages {
margin-bottom: 1.5rem;
background: #fff4f4;
border: 1px solid #ffccd2;
border-radius: 16px;
padding: 0.75rem 1rem;
color: #b42318;
}
.auth-message {
font-size: 0.85rem;
margin: 0.25rem 0;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-field {
display: flex;
flex-direction: column;
gap: 0.45rem;
font-size: 0.85rem;
color: #374151;
}
.auth-field input {
padding: 0.7rem 0.9rem;
border-radius: 12px;
border: 1px solid #d1d5db;
font-size: 0.95rem;
font-family: inherit;
}
.auth-field input:focus {
outline: 2px solid rgba(76, 110, 245, 0.35);
border-color: #6d7bf7;
}
.auth-submit {
margin-top: 0.5rem;
padding: 0.8rem 1rem;
border-radius: 999px;
border: none;
background: linear-gradient(130deg, #f97316, #facc15);
color: #1f2937;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.auth-submit:hover {
transform: translateY(-1px);
box-shadow: 0 12px 25px rgba(249, 115, 22, 0.3);
}
.auth-footer {
margin-top: 1.5rem;
font-size: 0.85rem;
color: #6b7280;
}
.auth-footer a {
color: #111827;
font-weight: 600;
text-decoration: none;
}
.auth-footer a:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,37 @@
{% extends 'frontend/base.html' %}
{% block title %}Login{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Welcome back</h1>
<p class="auth-subtitle">Log in to continue your flight plan.</p>
</header>
{% if messages %}
<div class="auth-messages">
{% for message in messages %}
<p class="auth-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" class="auth-form">
{% csrf_token %}
<label class="auth-field">
<span>Email</span>
<input type="email" name="email" autocomplete="email" required>
</label>
<label class="auth-field">
<span>Password</span>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit" class="auth-submit">Log in</button>
</form>
<footer class="auth-footer">
<p>No account yet? <a href="{% url 'register' %}">Create one</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'frontend/base.html' %}
{% block title %}Logout{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Log out</h1>
<p class="auth-subtitle">Are you sure you want to end this session?</p>
</header>
<form method="post" class="auth-form">
{% csrf_token %}
<button type="submit" class="auth-submit">Confirm logout</button>
</form>
<footer class="auth-footer">
<p>Changed your mind? <a href="{% url 'home' %}">Return home</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends 'frontend/base.html' %}
{% block title %}Register{% endblock %}
{% block content %}
<main class="auth-shell">
<section class="auth-card">
<header class="auth-header">
<p class="auth-kicker">DroneWars</p>
<h1 class="auth-title">Create your pilot profile</h1>
<p class="auth-subtitle">Sign up with your email and a secure password.</p>
</header>
{% if messages %}
<div class="auth-messages">
{% for message in messages %}
<p class="auth-message">{{ message }}</p>
{% endfor %}
</div>
{% endif %}
<form method="post" class="auth-form">
{% csrf_token %}
<label class="auth-field">
<span>Email</span>
<input type="email" name="email" autocomplete="email" required>
</label>
<label class="auth-field">
<span>Display name</span>
<input type="text" name="display_name" maxlength="150" autocomplete="nickname" required>
</label>
<label class="auth-field">
<span>Password</span>
<input type="password" name="password" autocomplete="new-password" required>
</label>
<label class="auth-field">
<span>Confirm password</span>
<input type="password" name="password_confirm" autocomplete="new-password" required>
</label>
<button type="submit" class="auth-submit">Create account</button>
</form>
<footer class="auth-footer">
<p>Already have an account? <a href="{% url 'login' %}">Log in</a>.</p>
</footer>
</section>
</main>
{% endblock %}

View File

@@ -5,4 +5,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.home, name="home"), path("", views.home, name="home"),
path("auth/register/", views.register, name="register"),
path("auth/login/", views.login, name="login"),
path("auth/logout/", views.logout, name="logout"),
] ]

View File

@@ -1,5 +1,105 @@
from django.shortcuts import render from django.contrib import messages
from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods
from django_ratelimit.decorators import ratelimit
from .models import UserProfile
def home(request): def home(request):
return render(request, "frontend/home.html") return render(request, "frontend/home.html")
@ratelimit(key="ip", rate="5/m", method="POST", block=False)
@require_http_methods(["GET", "POST"])
def register(request: HttpRequest) -> HttpResponse:
user = getattr(request, "user", None)
if user and user.is_authenticated:
return redirect("home")
if getattr(request, "limited", False):
messages.error(request, "Too many attempts. Try again in a minute.")
if request.method == "POST" and not getattr(request, "limited", False):
email = str(request.POST.get("email", "")).strip().lower()
display_name = str(request.POST.get("display_name", "")).strip()
password = request.POST.get("password") or ""
password_confirm = request.POST.get("password_confirm") or ""
errors: list[str] = []
if not email:
errors.append("Email is required.")
else:
try:
validate_email(email)
except ValidationError:
errors.append("Enter a valid email address.")
if not display_name:
errors.append("Display name is required.")
if not password:
errors.append("Password is required.")
elif password != password_confirm:
errors.append("Passwords do not match.")
if email and User.objects.filter(username=email).exists():
errors.append("An account with that email already exists.")
if errors:
for error in errors:
messages.error(request, error)
else:
user = User.objects.create_user(
username=email, email=email, password=password
)
UserProfile.objects.filter(user=user).update( # type: ignore[attr-defined]
display_name=display_name
)
auth_login(request, user)
return redirect("home")
return render(request, "frontend/register.html")
@ratelimit(key="ip", rate="10/m", method="POST", block=False)
@require_http_methods(["GET", "POST"])
def login(request: HttpRequest) -> HttpResponse:
user = getattr(request, "user", None)
if user and user.is_authenticated:
return redirect("home")
if getattr(request, "limited", False):
messages.error(request, "Too many attempts. Try again in a minute.")
if request.method == "POST" and not getattr(request, "limited", False):
email = str(request.POST.get("email", "")).strip().lower()
password = request.POST.get("password") or ""
if not email or not password:
messages.error(request, "Email and password are required.")
else:
user = authenticate(request, username=email, password=password)
if user is None:
messages.error(request, "Invalid email or password.")
else:
auth_login(request, user)
return redirect("home")
return render(request, "frontend/login.html")
@require_http_methods(["GET", "POST"])
def logout(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
auth_logout(request)
return redirect("home")
return render(request, "frontend/logout.html")

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"django>=6.0.2", "django>=6.0.2",
"django-ratelimit>=4.1.0",
"djangorestframework>=3.16.1", "djangorestframework>=3.16.1",
"djangorestframework-simplejwt>=5.5.0", "djangorestframework-simplejwt>=5.5.0",
"pydantic>=2.12.5", "pydantic>=2.12.5",

View File

@@ -2,6 +2,7 @@ annotated-types==0.7.0
asgiref==3.11.1 asgiref==3.11.1
click==8.3.1 click==8.3.1
django==6.0.2 django==6.0.2
django-ratelimit==4.1.0
djangorestframework==3.16.1 djangorestframework==3.16.1
djangorestframework-simplejwt==5.5.1 djangorestframework-simplejwt==5.5.1
h11==0.16.0 h11==0.16.0

11
uv.lock generated
View File

@@ -55,6 +55,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" }, { url = "https://files.pythonhosted.org/packages/96/ba/a6e2992bc5b8c688249c00ea48cb1b7a9bc09839328c81dc603671460928/django-6.0.2-py3-none-any.whl", hash = "sha256:610dd3b13d15ec3f1e1d257caedd751db8033c5ad8ea0e2d1219a8acf446ecc6", size = 8339381, upload-time = "2026-02-03T13:50:15.501Z" },
] ]
[[package]]
name = "django-ratelimit"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/8f/94038fe739b095aca3e4708ecc8a4e77f1fcfd87bed5d6baff43d4c80bc4/django-ratelimit-4.1.0.tar.gz", hash = "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", size = 11551, upload-time = "2023-07-24T20:34:32.374Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/78/2c59b30cd8bc8068d02349acb6aeed5c4e05eb01cdf2107ccd76f2e81487/django_ratelimit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d047a31cf94d83ef1465d7543ca66c6fc16695559b5f8d814d1b51df15110b92", size = 11608, upload-time = "2023-07-24T20:34:31.362Z" },
]
[[package]] [[package]]
name = "djangorestframework" name = "djangorestframework"
version = "3.16.1" version = "3.16.1"
@@ -87,6 +96,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
{ name = "django-ratelimit" },
{ name = "djangorestframework" }, { name = "djangorestframework" },
{ name = "djangorestframework-simplejwt" }, { name = "djangorestframework-simplejwt" },
{ name = "pydantic" }, { name = "pydantic" },
@@ -97,6 +107,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "django", specifier = ">=6.0.2" }, { name = "django", specifier = ">=6.0.2" },
{ name = "django-ratelimit", specifier = ">=4.1.0" },
{ name = "djangorestframework", specifier = ">=3.16.1" }, { name = "djangorestframework", specifier = ">=3.16.1" },
{ name = "djangorestframework-simplejwt", specifier = ">=5.5.0" }, { name = "djangorestframework-simplejwt", specifier = ">=5.5.0" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },