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

View File

@@ -2,4 +2,7 @@ from django.apps import 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.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%;
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 = [
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):
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")