add register, login, logout views
This commit is contained in:
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@@ -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
|
||||||
|
|||||||
37
frontend/migrations/0001_userprofile.py
Normal file
37
frontend/migrations/0001_userprofile.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
37
frontend/templates/frontend/login.html
Normal file
37
frontend/templates/frontend/login.html
Normal 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 %}
|
||||||
22
frontend/templates/frontend/logout.html
Normal file
22
frontend/templates/frontend/logout.html
Normal 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 %}
|
||||||
45
frontend/templates/frontend/register.html
Normal file
45
frontend/templates/frontend/register.html
Normal 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 %}
|
||||||
@@ -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"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
11
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user