working base site setup with gatekeeping working
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 6.0.3 on 2026-08-03 17:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('profiles', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='apitoken',
|
||||
index=models.Index(fields=['user', 'key_prefix'], name='profiles_ap_user_id_c2920d_idx'),
|
||||
),
|
||||
]
|
||||
@@ -32,5 +32,10 @@ class ApiToken(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
last_used = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['user', 'key_prefix']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.user.username} - {self.label} ({self.key_prefix}...)'
|
||||
@@ -1,13 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Login - Packs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Login</h1>
|
||||
<form method="post" action="{% url 'profiles:login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
<p>No account? <a href="{% url 'profiles:register' %}">Register</a></p>
|
||||
{% endblock %}
|
||||
@@ -1,13 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Register - Packs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Register</h1>
|
||||
<form method="post" action="{% url 'profiles:register' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
<p>Already have an account? <a href="{% url 'profiles:login' %}">Log in</a></p>
|
||||
{% endblock %}
|
||||
@@ -1,56 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Settings - Packs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Settings</h1>
|
||||
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h2>API Tokens</h2>
|
||||
<p>Tokens let non-browser clients (like a Fabric mod) access the API and download packs.</p>
|
||||
|
||||
<form method="post" action="{% url 'profiles:token_create' %}" class="token-create">
|
||||
{% csrf_token %}
|
||||
<input type="text" name="label" placeholder="Token label (e.g. Fabric updater)" required>
|
||||
<button type="submit">Create token</button>
|
||||
</form>
|
||||
|
||||
{% if tokens %}
|
||||
<table class="token-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Token</th>
|
||||
<th>Created</th>
|
||||
<th>Last used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in tokens %}
|
||||
<tr>
|
||||
<td>{{ token.label }}</td>
|
||||
<td><code>{{ token.token }}</code></td>
|
||||
<td>{{ token.created_at }}</td>
|
||||
<td>{{ token.last_used|default:"never" }}</td>
|
||||
<td>
|
||||
<form method="post" action="{% url 'profiles:token_delete' token.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit">Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No tokens yet.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,76 @@
|
||||
import secrets
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from .models import ApiToken, UserProfile
|
||||
|
||||
|
||||
class ApiTokenAuthTests(TestCase):
|
||||
"""Credential auth: 'Authorization: Bearer <username> <token>'."""
|
||||
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
self.token = secrets.token_urlsafe(48)
|
||||
ApiToken.objects.create(
|
||||
user=self.alice,
|
||||
token=self.token,
|
||||
key_prefix=self.token[:8],
|
||||
label='test',
|
||||
)
|
||||
|
||||
def _auth(self, credential=None, username=None, token=None):
|
||||
if credential is None:
|
||||
credential = f'{username or ""} {token or ""}'.strip()
|
||||
return self.client.get(
|
||||
'/home/',
|
||||
HTTP_AUTHORIZATION=f'Bearer {credential}',
|
||||
HTTP_ACCEPT='application/json',
|
||||
)
|
||||
|
||||
def test_valid_credentials_pass(self):
|
||||
resp = self._auth(username='Alice', token=self.token)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_wrong_username_rejected(self):
|
||||
# A valid token presented under another user's name must not work.
|
||||
resp = self._auth(username='Bob', token=self.token)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_wrong_token_rejected(self):
|
||||
resp = self._auth(username='Alice', token='x' * 64)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_revoked_token_rejected(self):
|
||||
ApiToken.objects.all().delete()
|
||||
resp = self._auth(username='Alice', token=self.token)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_missing_username_rejected(self):
|
||||
resp = self._auth(token=self.token)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_missing_credentials_rejected(self):
|
||||
resp = self._auth()
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_username_case_insensitive(self):
|
||||
resp = self._auth(username='aLiCe', token=self.token)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_unknown_user_rejected(self):
|
||||
resp = self._auth(username='Nobody', token=self.token)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_authenticates_as_owner(self):
|
||||
self.client.get(
|
||||
'/home/',
|
||||
HTTP_AUTHORIZATION=f'Bearer Alice {self.token}',
|
||||
HTTP_ACCEPT='application/json',
|
||||
)
|
||||
token = ApiToken.objects.get(token=self.token)
|
||||
self.assertIsNotNone(token.last_used)
|
||||
|
||||
@@ -17,7 +17,7 @@ def register(request):
|
||||
UserProfile.objects.get_or_create(user=user)
|
||||
login(request, user)
|
||||
messages.success(request, 'Registration successful.')
|
||||
return redirect('home')
|
||||
return redirect('landing:home')
|
||||
else:
|
||||
form = UserCreationForm()
|
||||
return render(request, 'profiles/register.html', {'form': form})
|
||||
@@ -29,7 +29,7 @@ def user_login(request):
|
||||
if form.is_valid():
|
||||
user = form.get_user()
|
||||
login(request, user)
|
||||
return redirect('home')
|
||||
return redirect('landing:home')
|
||||
else:
|
||||
form = AuthenticationForm(request)
|
||||
return render(request, 'profiles/login.html', {'form': form})
|
||||
@@ -38,8 +38,8 @@ def user_login(request):
|
||||
def user_logout(request):
|
||||
if request.method == 'POST':
|
||||
logout(request)
|
||||
return redirect('home')
|
||||
return redirect('home')
|
||||
return redirect('landing:home')
|
||||
return redirect('landing:home')
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
Reference in New Issue
Block a user