feat: add JWT auth

This commit is contained in:
2025-09-21 00:47:53 +08:00
parent 16a29ee12b
commit c4aef881bc
17 changed files with 312 additions and 93 deletions
+1 -1
View File
@@ -196,7 +196,7 @@ class EmailNotificationService:
])
for it in data_items:
plain_lines.append(f"- {it['label']}: {it['value']}")
plain_lines.append("\n此邮件由爱特工作室物品管理及财务管理系统自动发送")
plain_lines.append("\n此邮件由爱特工作室管理系统自动发送")
plain_message = "\n".join(plain_lines)
# 发送邮件到所有启用的通知邮箱
@@ -53,7 +53,7 @@
{% endif %}
<div class="footer">
此邮件由「爱特工作室物品管理及财务管理系统」自动发送,请勿直接回复。
此邮件由「爱特工作室管理系统」自动发送,请勿直接回复。
</div>
</div>
</body>
+26 -27
View File
@@ -1,23 +1,24 @@
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
import json
import re
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .services import EmailNotificationService
@csrf_exempt
@require_http_methods(["POST", "GET"])
@api_view(["GET", "POST"])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def notification_settings(request):
"""通知设置API"""
"""通知设置API(需要JWT"""
if request.method == 'GET':
try:
settings_data = EmailNotificationService.get_notification_settings()
all_emails = EmailNotificationService.get_all_notification_emails()
return JsonResponse({
return Response({
'success': True,
'data': {
'email_enabled': settings_data.get('email_enabled', False),
@@ -26,11 +27,11 @@ def notification_settings(request):
}
})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
return Response({'success': False, 'error': str(e)}, status=500)
elif request.method == 'POST':
try:
data = json.loads(request.body)
data = request.data if hasattr(request, 'data') else json.loads(request.body or '{}')
emails_to_update = data.get('notification_emails')
email_enabled_status = data.get('email_enabled')
@@ -38,7 +39,7 @@ def notification_settings(request):
was_updated = False
# 1. 如果请求中包含 'notification_emails' 键,则处理邮箱列表
if emails_to_update is not None: # 允许 emails_to_update 为空列表 []
if emails_to_update is not None: # 允许空列表
email_regex = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
valid_emails_data = []
@@ -70,7 +71,7 @@ def notification_settings(request):
# 执行更新成功,返回成功
if was_updated:
return JsonResponse({
return Response({
'success': True,
'message': '通知设置已更新',
# 返回最新的数据状态
@@ -81,31 +82,31 @@ def notification_settings(request):
})
else:
# 如果请求体为空或不包含任何有效键,则返回错误
return JsonResponse({'success': False, 'error': '未提供任何有效的更新数据'}, status=400)
return Response({'success': False, 'error': '未提供任何有效的更新数据'}, status=400)
except json.JSONDecodeError:
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
return Response({'success': False, 'error': str(e)}, status=500)
return JsonResponse({'success': False, 'error': '不支持的请求方法'}, status=405)
return Response({'success': False, 'error': '不支持的请求方法'}, status=405)
@csrf_exempt
@require_http_methods(["POST"])
@api_view(["POST"])
@authentication_classes([JWTAuthentication])
@permission_classes([IsAuthenticated])
def toggle_email_status(request):
"""切换邮箱启用状态API"""
"""切换邮箱启用状态API(需要JWT"""
try:
data = json.loads(request.body)
data = request.data if hasattr(request, 'data') else json.loads(request.body or '{}')
email_id = data.get('email_id')
is_enabled = data.get('is_enabled', True)
if not email_id:
return JsonResponse({'success': False, 'error': '缺少邮箱ID'}, status=400)
return Response({'success': False, 'error': '缺少邮箱ID'}, status=400)
success = EmailNotificationService.toggle_email_status(email_id, is_enabled)
if success:
return JsonResponse({
return Response({
'success': True,
'message': f'邮箱状态已更新为{"启用" if is_enabled else "禁用"}',
'data': {
@@ -113,9 +114,7 @@ def toggle_email_status(request):
}
})
else:
return JsonResponse({'success': False, 'error': '更新邮箱状态失败'}, status=500)
return Response({'success': False, 'error': '更新邮箱状态失败'}, status=500)
except json.JSONDecodeError:
return JsonResponse({'success': False, 'error': '无效的JSON数据'}, status=400)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
return Response({'success': False, 'error': str(e)}, status=500)
+5
View File
@@ -5,6 +5,7 @@ from django.utils import timezone
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from .models import FinancialRecord, Department, Category, ProofImage
from .serializers import (
@@ -20,6 +21,7 @@ class FinancialRecordViewSet(viewsets.ModelViewSet):
"""
获取财务记录
"""
authentication_classes = [JWTAuthentication]
queryset = FinancialRecord.objects.all()
def get_serializer_class(self):
@@ -185,6 +187,7 @@ class ProofImageViewSet(viewsets.ModelViewSet):
"""
凭证API
"""
authentication_classes = [JWTAuthentication]
queryset = ProofImage.objects.all()
serializer_class = ProofImageSerializer
@@ -283,6 +286,7 @@ class DepartmentViewSet(viewsets.ModelViewSet):
"""
获取部门
"""
authentication_classes = [JWTAuthentication]
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
@@ -346,5 +350,6 @@ class CategoryViewSet(viewsets.ModelViewSet):
"""
获取分类
"""
authentication_classes = [JWTAuthentication]
queryset = Category.objects.all()
serializer_class = CategorySerializer
+14 -2
View File
@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
import json
import os
from datetime import timedelta
from pathlib import Path
# SECURE 文件用来存储敏感信息,如 SECRET_KEYSMTP信息 等
@@ -134,13 +135,24 @@ AUTH_PASSWORD_VALIDATORS = [
# REST Framework configuration
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
}
# JWT 配置(可根据需要调整过期时间)
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"AUTH_HEADER_TYPES": ("Bearer",),
}
# CORS settings
CORS_ALLOWED_ORIGINS = [
+3
View File
@@ -19,6 +19,7 @@ from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path("admin/", admin.site.urls),
@@ -27,6 +28,8 @@ urlpatterns = [
path("", include("email_notice.urls")),
path("", include("personnel.urls")),
path("api-auth/", include("rest_framework.urls")),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
if settings.DEBUG:
+5
View File
@@ -3,6 +3,7 @@ from django.utils import timezone
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from .models import Item, ItemUsage, Category
from .serializers import (
@@ -13,6 +14,7 @@ from .serializers import (
class ItemViewSet(viewsets.ModelViewSet):
"""物品管理API"""
authentication_classes = [JWTAuthentication]
queryset = Item.objects.all()
serializer_class = ItemSerializer
@@ -107,6 +109,7 @@ class ItemViewSet(viewsets.ModelViewSet):
class ItemUsageViewSet(viewsets.ModelViewSet):
"""使用记录管理API"""
authentication_classes = [JWTAuthentication]
queryset = ItemUsage.objects.all()
serializer_class = ItemUsageSerializer
@@ -130,11 +133,13 @@ class ItemUsageViewSet(viewsets.ModelViewSet):
class CategoryViewSet(viewsets.ModelViewSet):
"""物品类别管理API"""
authentication_classes = [JWTAuthentication]
queryset = Category.objects.all()
serializer_class = CategorySerializer
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""用户管理API(只读)"""
authentication_classes = [JWTAuthentication]
queryset = User.objects.all()
serializer_class = UserSerializer
+3
View File
@@ -6,6 +6,7 @@ from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from .filters import PersonnelFilter
from .models import Personnel, ProjectGroup
@@ -20,6 +21,7 @@ logger = logging.getLogger(__name__)
class PersonnelViewSet(viewsets.ModelViewSet):
"""人员信息视图集"""
authentication_classes = [JWTAuthentication]
queryset = Personnel.objects.all()
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_class = PersonnelFilter
@@ -191,6 +193,7 @@ class PersonnelViewSet(viewsets.ModelViewSet):
class ProjectGroupViewSet(viewsets.ModelViewSet):
"""项目组视图集"""
authentication_classes = [JWTAuthentication]
queryset = ProjectGroup.objects.all()
serializer_class = ProjectGroupSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
Binary file not shown.