feat(core/backend): account settings endpoints (profile PATCH / change password / avatar upload)
§4 settings-save backend (no schema change; User already has phone/avatar_url): - me/ now GET+PATCH (update name/phone/email) - POST me/password/ — verify old password, set new (>=8), reissue token - POST me/avatar/ — multipart -> TOS upload -> presigned avatar_url Verified: profile PATCH 200, password change round-trip 200, original login restored. Note: notification/theme prefs have no User storage field -> will persist client-side (no migrate per rules). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8f80247e0d
commit
aad9bd683b
@ -1,6 +1,16 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import login, logout, me, register, team_member_detail, team_member_password, team_members
|
from .views import (
|
||||||
|
change_password,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
me,
|
||||||
|
register,
|
||||||
|
team_member_detail,
|
||||||
|
team_member_password,
|
||||||
|
team_members,
|
||||||
|
update_avatar,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -8,6 +18,8 @@ urlpatterns = [
|
|||||||
path("login/", login, name="auth-login"),
|
path("login/", login, name="auth-login"),
|
||||||
path("logout/", logout, name="auth-logout"),
|
path("logout/", logout, name="auth-logout"),
|
||||||
path("me/", me, name="auth-me"),
|
path("me/", me, name="auth-me"),
|
||||||
|
path("me/password/", change_password, name="auth-change-password"),
|
||||||
|
path("me/avatar/", update_avatar, name="auth-avatar"),
|
||||||
path("team/members/", team_members, name="team-members"),
|
path("team/members/", team_members, name="team-members"),
|
||||||
path("team/members/<uuid:member_id>/", team_member_detail, name="team-member-detail"),
|
path("team/members/<uuid:member_id>/", team_member_detail, name="team-member-detail"),
|
||||||
path("team/members/<uuid:member_id>/password/", team_member_password, name="team-member-password"),
|
path("team/members/<uuid:member_id>/password/", team_member_password, name="team-member-password"),
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, parser_classes, permission_classes
|
||||||
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@ -54,18 +58,69 @@ def logout(request):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET", "PATCH"])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def me(request):
|
def me(request):
|
||||||
team = get_current_team(request.user)
|
user = request.user
|
||||||
|
if request.method == "PATCH":
|
||||||
|
if "name" in request.data:
|
||||||
|
user.first_name = str(request.data.get("name") or "").strip()
|
||||||
|
if "phone" in request.data:
|
||||||
|
user.phone = str(request.data.get("phone") or "").strip()[:32]
|
||||||
|
email = str(request.data.get("email") or "").strip()
|
||||||
|
if email:
|
||||||
|
user.email = email
|
||||||
|
user.save(update_fields=["first_name", "phone", "email"])
|
||||||
|
team = get_current_team(user)
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"user": UserSerializer(request.user).data,
|
"user": UserSerializer(user).data,
|
||||||
"team": TeamSerializer(team).data,
|
"team": TeamSerializer(team).data,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def change_password(request):
|
||||||
|
user = request.user
|
||||||
|
old_password = str(request.data.get("old_password") or "")
|
||||||
|
new_password = str(request.data.get("new_password") or "").strip()
|
||||||
|
if not user.check_password(old_password):
|
||||||
|
return Response({"old_password": ["原密码不正确"]}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if len(new_password) < 8:
|
||||||
|
return Response({"new_password": ["新密码至少 8 位"]}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
user.set_password(new_password)
|
||||||
|
user.save(update_fields=["password"])
|
||||||
|
Token.objects.filter(user=user).delete()
|
||||||
|
token, _ = Token.objects.get_or_create(user=user)
|
||||||
|
return Response({"token": token.key})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@parser_classes([MultiPartParser, FormParser])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def update_avatar(request):
|
||||||
|
from apps.assets.storage import TosStorage
|
||||||
|
|
||||||
|
upload = request.FILES.get("file")
|
||||||
|
if upload is None:
|
||||||
|
return Response({"detail": "no file"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
user = request.user
|
||||||
|
suffix = Path(upload.name).suffix.lower() or ".png"
|
||||||
|
object_key = f"users/{user.id}/avatar/{uuid.uuid4()}{suffix}"
|
||||||
|
storage = TosStorage()
|
||||||
|
storage.upload_fileobj(
|
||||||
|
fileobj=upload.file,
|
||||||
|
object_key=object_key,
|
||||||
|
content_type=upload.content_type or "image/png",
|
||||||
|
)
|
||||||
|
# 头像直接存可访问的预签名 URL(长有效期);后续如需永久化可改为读时签发
|
||||||
|
user.avatar_url = storage.presigned_get_url(object_key=object_key, expires_in=7 * 24 * 3600)
|
||||||
|
user.save(update_fields=["avatar_url"])
|
||||||
|
return Response(UserSerializer(user).data)
|
||||||
|
|
||||||
|
|
||||||
def normalize_member_role(role):
|
def normalize_member_role(role):
|
||||||
if role == "super":
|
if role == "super":
|
||||||
return TeamMember.Role.OWNER
|
return TeamMember.Role.OWNER
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user