This commit is contained in:
Jakub Kaniecki
2025-05-18 16:23:03 +02:00
commit 12c76e3e5a
220 changed files with 31696 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Created by venv; see https://docs.python.org/3/library/venv.html
/bin
/lib
/include
/share
/man
/doc
/html
pyvenv.cfg
View File
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ContentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'content'
@@ -0,0 +1,59 @@
# Generated by Django 4.2 on 2025-05-01 18:16
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)),
],
),
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(max_length=2000)),
('approved', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Page',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('content', models.TextField()),
('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('content', models.TextField()),
('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending Review'), ('published', 'Published')], default='draft', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'abstract': False,
},
),
]
@@ -0,0 +1,53 @@
# Generated by Django 4.2 on 2025-05-01 18:16
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),
('content', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='post',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='post',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='page',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='page',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='content.category'),
),
migrations.AddField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='comment',
name='page',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.page'),
),
migrations.AddField(
model_name='comment',
name='post',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='content.post'),
),
]
@@ -0,0 +1,30 @@
from django.db import migrations
def create_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
default_categories = [
{'name': 'General', 'slug': 'general'},
{'name': 'Technology', 'slug': 'technology'},
{'name': 'Science', 'slug': 'science'},
{'name': 'Programming', 'slug': 'programming'},
{'name': 'News', 'slug': 'news'},
]
for category in default_categories:
Category.objects.get_or_create(
name=category['name'],
slug=category['slug']
)
def remove_initial_categories(apps, schema_editor):
Category = apps.get_model('content', 'Category')
Category.objects.filter(slug__in=['general', 'technology', 'science', 'programming', 'news']).delete()
class Migration(migrations.Migration):
dependencies = [
('content', '0001_initial'),
]
operations = [
migrations.RunPython(create_initial_categories, remove_initial_categories),
]
@@ -0,0 +1,31 @@
# Generated by Django 4.2 on 2025-05-01 19:05
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('content', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='UploadedImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='uploads/%Y/%m/%d/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])])),
('title', models.CharField(max_length=255)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('file_size', models.PositiveIntegerField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-uploaded_at'],
},
),
]
@@ -0,0 +1,14 @@
# Generated by Django 4.2 on 2025-05-01 19:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('content', '0002_initial_categories'),
('content', '0003_uploadedimage'),
]
operations = [
]
@@ -0,0 +1,31 @@
# Generated by Django 4.2 on 2025-05-04 10:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0004_merge_0002_initial_categories_0003_uploadedimage'),
]
operations = [
migrations.CreateModel(
name='Tag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('slug', models.SlugField(unique=True)),
],
),
migrations.AddField(
model_name='page',
name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'),
),
migrations.AddField(
model_name='post',
name='tags',
field=models.ManyToManyField(blank=True, to='content.tag'),
),
]
@@ -0,0 +1,23 @@
# Generated by Django 4.2 on 2025-05-10 23:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0005_tag_page_tags_post_tags'),
]
operations = [
migrations.AddField(
model_name='page',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
migrations.AddField(
model_name='post',
name='slug',
field=models.SlugField(blank=True, unique=True),
),
]
+88
View File
@@ -0,0 +1,88 @@
from django.db import models
from users.models import User
from django.conf import settings
from django.utils import timezone
from django.core.validators import FileExtensionValidator
class Category(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class AbstractContent(models.Model):
STATUS_CHOICES = (
('draft', 'Draft'),
('pending', 'Pending Review'),
('published', 'Published'),
)
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True, blank=True)
content = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
tags = models.ManyToManyField('Tag', blank=True)
class Meta:
abstract = True
def __str__(self):
return self.title
class Post(AbstractContent):
pass
class Page(AbstractContent):
pass
class Comment(models.Model):
content = models.TextField(max_length=2000)
approved = models.BooleanField(default=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, null=True, blank=True, on_delete=models.CASCADE)
page = models.ForeignKey(Page, null=True, blank=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class UploadedImage(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
image = models.ImageField(
upload_to='uploads/%Y/%m/%d/',
validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png', 'gif', 'webp'])]
)
title = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True)
file_size = models.PositiveIntegerField() # Size in bytes
class Meta:
ordering = ['-uploaded_at']
def __str__(self):
return f"{self.title} - {self.user.username}"
@property
def url(self):
return self.image.url if self.image else None
def save(self, *args, **kwargs):
if not self.file_size and self.image:
self.file_size = self.image.size
super().save(*args, **kwargs)
@classmethod
def get_user_daily_upload_size(cls, user):
today = timezone.now().date()
tomorrow = today + timezone.timedelta(days=1)
return cls.objects.filter(
user=user,
uploaded_at__gte=today,
uploaded_at__lt=tomorrow
).aggregate(total_size=models.Sum('file_size'))['total_size'] or 0
class Tag(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
+115
View File
@@ -0,0 +1,115 @@
from rest_framework import serializers
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from users.models import User
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'slug']
read_only_fields = ['id']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
read_only_fields = ['id']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'content', 'author', 'post', 'page', 'approved', 'created_at']
read_only_fields = ['id', 'created_at', 'author']
class PostSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
comments = CommentSerializer(many=True, read_only=True)
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
source='tags',
many=True,
write_only=True,
required=False
)
slug = serializers.SlugField(required=True)
class Meta:
model = Post
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'created_at', 'updated_at', 'tags', 'tag_ids', 'slug'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
class PageSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all(),
source='category',
write_only=True
)
comments = CommentSerializer(many=True, read_only=True)
slug = serializers.SlugField(required=True)
class Meta:
model = Page
fields = [
'id', 'title', 'content', 'status', 'author',
'category', 'category_id', 'comments',
'created_at', 'updated_at', 'slug'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'author', 'slug']
lookup_field = 'slug'
extra_kwargs = {
'url': {'lookup_field': 'slug'}
}
class UploadedImageSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
file_size_display = serializers.SerializerMethodField()
image = serializers.ImageField(required=True)
class Meta:
model = UploadedImage
fields = ['id', 'title', 'image', 'url', 'uploaded_at', 'file_size', 'file_size_display']
read_only_fields = ['file_size', 'uploaded_at', 'url']
def get_url(self, obj):
request = self.context.get('request')
if obj.image and hasattr(obj.image, 'url'):
return request.build_absolute_uri(obj.image.url) if request else obj.image.url
return None
def get_file_size_display(self, obj):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if obj.file_size < 1024.0:
return f"{obj.file_size:.1f} {unit}"
obj.file_size /= 1024.0
return f"{obj.file_size:.1f} TB"
def validate_image(self, value):
if not value:
raise serializers.ValidationError("Image file is required.")
return value
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+14
View File
@@ -0,0 +1,14 @@
from rest_framework.routers import DefaultRouter
from django.urls import path, include
from .views import PostViewSet, CommentViewSet, PageViewSet, CategoryViewSet, UploadedImageViewSet, TagViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'comments', CommentViewSet, basename='comment')
router.register(r'pages', PageViewSet, basename='page')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'images', UploadedImageViewSet, basename='image')
router.register(r'tags', TagViewSet, basename='tag')
urlpatterns = [
path('', include(router.urls)),
]
+202
View File
@@ -0,0 +1,202 @@
from rest_framework import viewsets, status, filters
from rest_framework.response import Response
from rest_framework.decorators import action
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Comment, Page, Category, UploadedImage, Tag
from .serializers import PostSerializer, CommentSerializer, PageSerializer, CategorySerializer, UploadedImageSerializer, TagSerializer
from users.permissions import IsAuthorOrReadOnly, IsEditorOrAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.parsers import MultiPartParser, FormParser
from django.conf import settings
from rest_framework import serializers
class PostViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing blog posts.
list:
Return a list of all posts. Results can be filtered by:
- status (draft/pending/published)
- author
- category
- created_at (date range)
create:
Create a new post. Requires authentication and appropriate permissions.
retrieve:
Return the details of a specific post by ID.
update:
Update all fields of a specific post. Requires authentication and appropriate permissions.
partial_update:
Update one or more fields of a specific post. Requires authentication and appropriate permissions.
destroy:
Delete a specific post. Requires authentication and appropriate permissions.
"""
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
lookup_field = 'slug'
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Post.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Post.objects.filter(
author=self.request.user
)
return queryset
def perform_update(self, serializer):
print(serializer.validated_data)
return super().perform_update(serializer)
class CommentViewSet(viewsets.ModelViewSet):
"""
API endpoint for managing comments on posts and pages.
list:
Return a list of all comments. Results can be filtered by:
- post_id
- page_id
- author
- approved status
create:
Create a new comment. Requires authentication.
retrieve:
Return the details of a specific comment by ID.
update:
Update all fields of a specific comment. Requires authentication and appropriate permissions.
partial_update:
Update one or more fields of a specific comment. Requires authentication and appropriate permissions.
destroy:
Delete a specific comment. Requires authentication and appropriate permissions.
"""
queryset = Comment.objects.all()
serializer_class = CommentSerializer
permission_classes = [IsAuthorOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
filterset_fields = ['post', 'page', 'author', 'approved']
ordering_fields = ['created_at']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Comment.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(approved=True)
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
approved=True
) | Comment.objects.filter(
author=self.request.user
)
return queryset
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = [IsEditorOrAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['status', 'author', 'category']
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'updated_at', 'title']
ordering = ['-created_at']
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def get_queryset(self):
queryset = Page.objects.all()
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
elif not self.request.user.role in ['admin', 'moderator']:
queryset = queryset.filter(
status='published'
) | Page.objects.filter(
author=self.request.user
)
return queryset
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
def get_queryset(self):
return Category.objects.all()
class UploadedImageViewSet(viewsets.ModelViewSet):
serializer_class = UploadedImageSerializer
parser_classes = (MultiPartParser, FormParser)
def get_queryset(self):
return UploadedImage.objects.filter(user=self.request.user)
def get_serializer_context(self):
context = super().get_serializer_context()
context['request'] = self.request
return context
def perform_create(self, serializer):
# Check daily upload quota (5MB = 5 * 1024 * 1024 bytes)
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
# Get current daily upload size
current_size = UploadedImage.get_user_daily_upload_size(self.request.user)
file_size = self.request.FILES['image'].size
# Check if upload would exceed daily limit
if current_size + file_size > DAILY_UPLOAD_LIMIT:
raise serializers.ValidationError({
'image': 'Daily upload limit (5MB) exceeded. Please try again tomorrow.'
})
try:
instance = serializer.save(user=self.request.user, file_size=file_size)
except Exception as e:
raise
@action(detail=False, methods=['get'])
def quota(self, request):
DAILY_UPLOAD_LIMIT = 5 * 1024 * 1024 # 5MB in bytes
current_size = UploadedImage.get_user_daily_upload_size(request.user)
remaining = max(0, DAILY_UPLOAD_LIMIT - current_size)
return Response({
'daily_limit': DAILY_UPLOAD_LIMIT,
'used': current_size,
'remaining': remaining,
'daily_limit_display': f"{DAILY_UPLOAD_LIMIT / (1024 * 1024):.1f}MB",
'used_display': f"{current_size / (1024 * 1024):.1f}MB",
'remaining_display': f"{remaining / (1024 * 1024):.1f}MB"
})
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [filters.SearchFilter]
search_fields = ['name', 'slug']
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core'
View File
+3
View File
@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.
+60
View File
@@ -0,0 +1,60 @@
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'corsheaders',
'django_filters',
'users',
'content',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Binary file not shown.
View File
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for izaac2backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'izaac2backend.settings')
application = get_asgi_application()
+290
View File
@@ -0,0 +1,290 @@
"""
Django settings for izaac2backend project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
from pathlib import Path
from datetime import timedelta
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Custom user model
AUTH_USER_MODEL = 'users.User'
# Frontend URL for password reset links
FRONTEND_URL = 'http://localhost:5173' # Vite default port
# Email Configuration
if DEBUG:
# Development email settings (console backend)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEFAULT_FROM_EMAIL = 'noreply@izaac.com'
else:
# Production email settings (SMTP backend)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com' # Or your SMTP server
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = '' # Your email address
EMAIL_HOST_PASSWORD = '' # Your email password or app-specific password
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-^+2p2e93g2_1h-@u&l&j+(9n&6z02-e$pirm6(@@(4$+-zyhxr'
ALLOWED_HOSTS = []
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]
CORS_ALLOW_CREDENTIALS = True
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
),
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'JWK_URL': None,
'LEEWAY': 0,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',
'JTI_CLAIM': 'jti',
}
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'drf_spectacular',
'corsheaders',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'core',
'users',
'content',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'izaac2backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'izaac2backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Add Spectacular settings
SPECTACULAR_SETTINGS = {
'TITLE': 'Izaac API',
'DESCRIPTION': '''
API for Izaac platform - A content management system with user authentication.
## Authentication
This API uses JWT (JSON Web Token) for authentication. To authenticate:
1. Use the `/api/v1/users/login/` endpoint to get your access and refresh tokens
2. Include the access token in the Authorization header: `Bearer <your_access_token>`
3. When the access token expires, use the refresh token to get a new one via `/api/v1/users/refresh/`
## Rate Limiting
API requests are limited to 100 requests per minute per IP address.
''',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
'persistAuthorization': True,
'displayOperationId': True,
'filter': True,
'syntaxHighlight.activate': True,
'syntaxHighlight.theme': 'monokai',
},
'COMPONENT_SPLIT_REQUEST': True,
'TAGS': [
{
'name': 'users',
'description': 'User management endpoints including authentication and user operations'
},
{
'name': 'content',
'description': 'Content management endpoints for posts, comments, and other content types'
}
],
'SECURITY': [{'jwtAuth': []}],
'SECURITY_DEFINITIONS': {
'jwtAuth': {
'type': 'http',
'scheme': 'bearer',
'bearerFormat': 'JWT',
'description': 'JWT token obtained from the login endpoint'
}
},
'SERVE_AUTHENTICATION': None, # Allow public access to documentation
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], # Allow public access to documentation
'APPEND_COMPONENTS': {
'securitySchemes': {
'jwtAuth': {
'type': 'http',
'scheme': 'bearer',
'bearerFormat': 'JWT',
'description': 'JWT token obtained from the login endpoint'
}
}
},
'PREPROCESSING_HOOKS': [],
'POSTPROCESSING_HOOKS': [],
'SORT_OPERATIONS': True,
'SORT_OPERATION_PARAMETERS': True,
'ENUM_NAME_OVERRIDES': {
'StatusEnum': 'content.models.AbstractContent.STATUS_CHOICES'
},
'GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator',
'GENERATOR_CLASS_KWARGS': {
'servers': [
{'url': 'http://localhost:8000', 'description': 'Local development server'},
{'url': 'https://api.izaac.com', 'description': 'Production server'}
]
}
}
# Add CORS settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173", # Vite default port
"http://127.0.0.1:5173",
]
CORS_ALLOW_CREDENTIALS = True
# Media files (Uploads)
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Maximum upload size (5MB)
MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5MB in bytes
+37
View File
@@ -0,0 +1,37 @@
"""
URL configuration for izaac2backend project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView
from rest_framework.permissions import AllowAny
urlpatterns = [
path('admin/', admin.site.urls),
path('api/users/', include('users.urls')),
path('api/content/', include('content.urls')),
# path('api/v1/core/', include('core.urls')),
# API Documentation URLs
path('api/schema/', SpectacularAPIView.as_view(permission_classes=[AllowAny]), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema', permission_classes=[AllowAny]), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema', permission_classes=[AllowAny]), name='redoc'),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for izaac2backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'izaac2backend.settings')
application = get_wsgi_application()
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'izaac2backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

+8
View File
@@ -0,0 +1,8 @@
Django==4.2.0
djangorestframework>=3.14.0
django-cors-headers>=4.3.0
django-filter>=23.5
python-dotenv>=1.0.0
psycopg2-binary>=2.9.9
Pillow>=10.0.0
djangorestframework-simplejwt>=5.3.0
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'users'
+62
View File
@@ -0,0 +1,62 @@
# Generated by Django 4.2 on 2025-05-01 18:16
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('role', models.CharField(choices=[('admin', 'Admin'), ('moderator', 'Moderator'), ('user', 'User')], default='user', max_length=10)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='custom_user_set', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='PasswordResetToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=100, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('used', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Password Reset Token',
'verbose_name_plural': 'Password Reset Tokens',
},
),
]
+57
View File
@@ -0,0 +1,57 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils import timezone
import secrets
import string
# Create your models here.
class User(AbstractUser):
ROLES = (
('admin', 'Admin'),
('moderator', 'Moderator'),
('user', 'User'),
)
role = models.CharField(max_length=10, choices=ROLES, default='user')
# Add related_name to resolve clashes
groups = models.ManyToManyField(
'auth.Group',
related_name='custom_user_set',
blank=True,
help_text='The groups this user belongs to.',
verbose_name='groups',
)
user_permissions = models.ManyToManyField(
'auth.Permission',
related_name='custom_user_set',
blank=True,
help_text='Specific permissions for this user.',
verbose_name='user permissions',
)
def __str__(self):
return self.username
class PasswordResetToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
token = models.CharField(max_length=100, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
used = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if not self.token:
# Generate a random token
alphabet = string.ascii_letters + string.digits
self.token = ''.join(secrets.choice(alphabet) for _ in range(64))
super().save(*args, **kwargs)
def is_valid(self):
return (
not self.used and
self.expires_at > timezone.now()
)
class Meta:
verbose_name = 'Password Reset Token'
verbose_name_plural = 'Password Reset Tokens'
+17
View File
@@ -0,0 +1,17 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAuthorOrReadOnly(BasePermission):
"""
Pozwala edytować tylko własny wpis/komentarz.
"""
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.author == request.user
class IsEditorOrAdmin(BasePermission):
"""
Pozwala publikować treści tylko edytorom lub adminom.
"""
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.role in ['admin', 'editor']
+66
View File
@@ -0,0 +1,66 @@
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from .models import PasswordResetToken
User = get_user_model()
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
password2 = serializers.CharField(write_only=True, required=True)
class Meta:
model = User
fields = ('username', 'password', 'password2', 'email', 'first_name', 'last_name', 'role')
extra_kwargs = {
'first_name': {'required': True},
'last_name': {'required': True},
'email': {'required': True},
'role': {'default': 'user'}
}
def validate(self, attrs):
if attrs['password'] != attrs['password2']:
raise serializers.ValidationError({"password": "Password fields didn't match."})
return attrs
def create(self, validated_data):
validated_data.pop('password2')
user = User.objects.create_user(**validated_data)
return user
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'role')
read_only_fields = ('role', 'is_staff', 'is_active', 'is_superuser', 'date_joined', 'last_login', 'groups', 'user_permissions', 'id')
class ResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
def validate_email(self, value):
try:
User.objects.get(email=value)
except User.DoesNotExist:
# Don't reveal that the email doesn't exist
pass
return value
class ResetPasswordConfirmSerializer(serializers.Serializer):
token = serializers.UUIDField(required=True)
new_password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
confirm_password = serializers.CharField(write_only=True, required=True)
def validate(self, attrs):
if attrs['new_password'] != attrs['confirm_password']:
raise serializers.ValidationError({"new_password": "Password fields didn't match."})
try:
reset_token = PasswordResetToken.objects.get(token=attrs['token'])
if not reset_token.is_valid():
raise serializers.ValidationError({"token": "Invalid or expired token."})
except PasswordResetToken.DoesNotExist:
raise serializers.ValidationError({"token": "Invalid token."})
return attrs
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+22
View File
@@ -0,0 +1,22 @@
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from .views import (
RegisterView,
UserDetailView,
ChangePasswordView,
ResetPasswordView,
ResetPasswordConfirmView,
)
urlpatterns = [
path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('register/', RegisterView.as_view(), name='register'),
path('me/', UserDetailView.as_view(), name='user-detail'),
path('change-password/', ChangePasswordView.as_view(), name='change-password'),
path('reset-password/', ResetPasswordView.as_view(), name='reset-password'),
path('reset-password/confirm/', ResetPasswordConfirmView.as_view(), name='reset-password-confirm'),
]
+174
View File
@@ -0,0 +1,174 @@
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
from rest_framework.views import APIView
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import timedelta
from django.core.mail import send_mail
from django.conf import settings
from .serializers import UserRegistrationSerializer, UserSerializer, ResetPasswordSerializer, ResetPasswordConfirmSerializer
from .models import User, PasswordResetToken
import secrets
import string
User = get_user_model()
# Create your views here.
class RegisterView(generics.CreateAPIView):
"""
API endpoint for user registration.
create:
Register a new user with the following fields:
- username (required)
- password (required)
- password2 (required, must match password)
- email (required)
- first_name (required)
- last_name (required)
- role (optional, defaults to 'user')
"""
permission_classes = (AllowAny,)
serializer_class = UserRegistrationSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(
{"message": "User registered successfully. Please login to continue."},
status=status.HTTP_201_CREATED,
headers=headers
)
class UserViewSet(generics.ListAPIView):
"""
API endpoint for viewing and editing users.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [IsAdminUser]
class UserDetailView(generics.RetrieveUpdateAPIView):
"""
API endpoint for retrieving and updating the current user's data.
"""
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
def get_object(self):
return self.request.user
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
class ChangePasswordView(APIView):
"""
API endpoint for changing user password.
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
old_password = request.data.get('old_password')
new_password = request.data.get('new_password')
if not old_password or not new_password:
return Response(
{"detail": "Both old and new password are required."},
status=status.HTTP_400_BAD_REQUEST
)
if not user.check_password(old_password):
return Response(
{"detail": "Current password is incorrect."},
status=status.HTTP_400_BAD_REQUEST
)
try:
validate_password(new_password, user)
except ValidationError as e:
return Response(
{"detail": list(e.messages)},
status=status.HTTP_400_BAD_REQUEST
)
user.set_password(new_password)
user.save()
return Response(
{"detail": "Password successfully updated."},
status=status.HTTP_200_OK
)
class ResetPasswordView(generics.GenericAPIView):
permission_classes = [AllowAny]
serializer_class = ResetPasswordSerializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
email = serializer.validated_data['email']
try:
user = User.objects.get(email=email)
# Create a new reset token
token = PasswordResetToken.objects.create(
user=user,
expires_at=timezone.now() + timedelta(hours=24)
)
# Send reset email
reset_url = f"{settings.FRONTEND_URL}/reset-password/{token.token}"
send_mail(
'Password Reset Request',
f'Click the following link to reset your password: {reset_url}',
settings.DEFAULT_FROM_EMAIL,
[email],
fail_silently=False,
)
return Response({
'detail': 'If an account exists with this email, you will receive password reset instructions.'
}, status=status.HTTP_200_OK)
except User.DoesNotExist:
# Don't reveal that the email doesn't exist
return Response({
'detail': 'If an account exists with this email, you will receive password reset instructions.'
}, status=status.HTTP_200_OK)
class ResetPasswordConfirmView(generics.GenericAPIView):
permission_classes = [AllowAny]
serializer_class = ResetPasswordConfirmSerializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
token = serializer.validated_data['token']
new_password = serializer.validated_data['new_password']
reset_token = PasswordResetToken.objects.get(token=token)
user = reset_token.user
# Update password
user.set_password(new_password)
user.save()
# Mark token as used
reset_token.used = True
reset_token.save()
return Response({
'detail': 'Password has been reset successfully.'
}, status=status.HTTP_200_OK)