feat: add AirShelf core implementation
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .models import Team, TeamMember, User
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class AirShelfUserAdmin(UserAdmin):
|
||||
list_display = ("username", "email", "status", "is_staff", "date_joined")
|
||||
list_filter = ("status", "is_staff", "is_superuser")
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "owner", "status", "created_at")
|
||||
search_fields = ("name", "owner__username", "owner__email")
|
||||
list_filter = ("status",)
|
||||
|
||||
|
||||
@admin.register(TeamMember)
|
||||
class TeamMemberAdmin(admin.ModelAdmin):
|
||||
list_display = ("team", "user", "role", "status", "monthly_credit_limit")
|
||||
search_fields = ("team__name", "user__username", "user__email")
|
||||
list_filter = ("role", "status")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.accounts"
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
# Generated by Django 5.1.15 on 2026-05-29 03:59
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
("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"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[("active", "Active"), ("disabled", "Disabled")],
|
||||
default="active",
|
||||
max_length=24,
|
||||
),
|
||||
),
|
||||
("phone", models.CharField(blank=True, max_length=32)),
|
||||
("avatar_url", models.URLField(blank=True)),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
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="Team",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(max_length=128)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[("active", "Active"), ("disabled", "Disabled")],
|
||||
default="active",
|
||||
max_length=24,
|
||||
),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="owned_teams",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TeamMember",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"role",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("owner", "Owner"),
|
||||
("admin", "Admin"),
|
||||
("member", "Member"),
|
||||
("viewer", "Viewer"),
|
||||
],
|
||||
default="member",
|
||||
max_length=24,
|
||||
),
|
||||
),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("active", "Active"),
|
||||
("invited", "Invited"),
|
||||
("disabled", "Disabled"),
|
||||
],
|
||||
default="active",
|
||||
max_length=24,
|
||||
),
|
||||
),
|
||||
(
|
||||
"monthly_credit_limit",
|
||||
models.DecimalField(decimal_places=2, default=0, max_digits=12),
|
||||
),
|
||||
(
|
||||
"team",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="members",
|
||||
to="accounts.team",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="team_memberships",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("team", "user")},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import uuid
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from apps.common.models import TimeStampedModel
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
class Status(models.TextChoices):
|
||||
ACTIVE = "active", "Active"
|
||||
DISABLED = "disabled", "Disabled"
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
status = models.CharField(max_length=24, choices=Status.choices, default=Status.ACTIVE)
|
||||
phone = models.CharField(max_length=32, blank=True)
|
||||
avatar_url = models.URLField(blank=True)
|
||||
|
||||
@property
|
||||
def is_disabled(self) -> bool:
|
||||
return self.status == self.Status.DISABLED
|
||||
|
||||
|
||||
class Team(TimeStampedModel):
|
||||
class Status(models.TextChoices):
|
||||
ACTIVE = "active", "Active"
|
||||
DISABLED = "disabled", "Disabled"
|
||||
|
||||
name = models.CharField(max_length=128)
|
||||
status = models.CharField(max_length=24, choices=Status.choices, default=Status.ACTIVE)
|
||||
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name="owned_teams")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class TeamMember(TimeStampedModel):
|
||||
class Role(models.TextChoices):
|
||||
OWNER = "owner", "Owner"
|
||||
ADMIN = "admin", "Admin"
|
||||
MEMBER = "member", "Member"
|
||||
VIEWER = "viewer", "Viewer"
|
||||
|
||||
class Status(models.TextChoices):
|
||||
ACTIVE = "active", "Active"
|
||||
INVITED = "invited", "Invited"
|
||||
DISABLED = "disabled", "Disabled"
|
||||
|
||||
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="members")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="team_memberships")
|
||||
role = models.CharField(max_length=24, choices=Role.choices, default=Role.MEMBER)
|
||||
status = models.CharField(max_length=24, choices=Status.choices, default=Status.ACTIVE)
|
||||
monthly_credit_limit = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("team", "user")]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.team} / {self.user} / {self.role}"
|
||||
@@ -0,0 +1,68 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.billing.models import CreditAccount
|
||||
|
||||
from .models import Team, TeamMember, User
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "first_name", "last_name", "email", "phone", "avatar_url", "status"]
|
||||
read_only_fields = ["id", "status"]
|
||||
|
||||
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ["id", "name", "status", "owner", "created_at", "updated_at"]
|
||||
read_only_fields = ["id", "status", "owner", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
user = UserSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TeamMember
|
||||
fields = ["id", "team", "user", "role", "status", "monthly_credit_limit"]
|
||||
read_only_fields = ["id", "team", "user", "status"]
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(max_length=150)
|
||||
password = serializers.CharField(min_length=8, write_only=True)
|
||||
email = serializers.EmailField(required=False, allow_blank=True)
|
||||
team_name = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
|
||||
def validate_username(self, value):
|
||||
if User.objects.filter(username=value).exists():
|
||||
raise serializers.ValidationError("username already exists")
|
||||
return value
|
||||
|
||||
def create(self, validated_data):
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
with transaction.atomic():
|
||||
user = User.objects.create_user(
|
||||
username=validated_data["username"],
|
||||
password=validated_data["password"],
|
||||
email=validated_data.get("email", ""),
|
||||
)
|
||||
team = Team.objects.create(
|
||||
name=validated_data.get("team_name") or f"{user.username}'s Team",
|
||||
owner=user,
|
||||
)
|
||||
TeamMember.objects.create(team=team, user=user, role=TeamMember.Role.OWNER)
|
||||
CreditAccount.objects.create(
|
||||
team=team,
|
||||
balance=Decimal(str(settings.DEFAULT_TRIAL_CREDITS)),
|
||||
)
|
||||
return {"user": user, "team": team}
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField(write_only=True)
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.accounts.models import Team, TeamMember, User
|
||||
from apps.billing.models import CreditAccount
|
||||
|
||||
|
||||
class AuthApiTests(TestCase):
|
||||
def test_register_creates_user_team_member_credit_account_and_token(self):
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/api/auth/register/",
|
||||
{
|
||||
"username": "new-owner",
|
||||
"password": "strong-password",
|
||||
"email": "owner@example.com",
|
||||
"team_name": "Launch Team",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertIn("token", response.data)
|
||||
user = User.objects.get(username="new-owner")
|
||||
team = Team.objects.get(name="Launch Team")
|
||||
self.assertEqual(team.owner, user)
|
||||
self.assertTrue(TeamMember.objects.filter(team=team, user=user, role=TeamMember.Role.OWNER).exists())
|
||||
self.assertTrue(CreditAccount.objects.filter(team=team).exists())
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import login, logout, me, register, team_member_detail, team_member_password, team_members
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("register/", register, name="auth-register"),
|
||||
path("login/", login, name="auth-login"),
|
||||
path("logout/", logout, name="auth-logout"),
|
||||
path("me/", me, name="auth-me"),
|
||||
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>/password/", team_member_password, name="team-member-password"),
|
||||
]
|
||||
@@ -0,0 +1,170 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from django.db import transaction
|
||||
from rest_framework import status
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.common.api import get_current_team
|
||||
|
||||
from .models import TeamMember, User
|
||||
from .serializers import LoginSerializer, RegisterSerializer, TeamMemberSerializer, TeamSerializer, UserSerializer
|
||||
|
||||
|
||||
def auth_payload(user, team, token):
|
||||
return {
|
||||
"token": token.key,
|
||||
"user": UserSerializer(user).data,
|
||||
"team": TeamSerializer(team).data,
|
||||
}
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([])
|
||||
def register(request):
|
||||
serializer = RegisterSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.save()
|
||||
token, _ = Token.objects.get_or_create(user=data["user"])
|
||||
return Response(auth_payload(data["user"], data["team"], token), status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([])
|
||||
def login(request):
|
||||
serializer = LoginSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
user = authenticate(
|
||||
request,
|
||||
username=serializer.validated_data["username"],
|
||||
password=serializer.validated_data["password"],
|
||||
)
|
||||
if user is None or user.is_disabled:
|
||||
return Response({"detail": "invalid credentials"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
team = get_current_team(user)
|
||||
token, _ = Token.objects.get_or_create(user=user)
|
||||
return Response(auth_payload(user, team, token))
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def logout(request):
|
||||
Token.objects.filter(user=request.user).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def me(request):
|
||||
team = get_current_team(request.user)
|
||||
return Response(
|
||||
{
|
||||
"user": UserSerializer(request.user).data,
|
||||
"team": TeamSerializer(team).data,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def normalize_member_role(role):
|
||||
if role == "super":
|
||||
return TeamMember.Role.OWNER
|
||||
if role in {TeamMember.Role.OWNER, TeamMember.Role.ADMIN, TeamMember.Role.MEMBER, TeamMember.Role.VIEWER}:
|
||||
return role
|
||||
return TeamMember.Role.MEMBER
|
||||
|
||||
|
||||
def can_manage_team(user, team):
|
||||
member = TeamMember.objects.filter(team=team, user=user, status=TeamMember.Status.ACTIVE).first()
|
||||
return bool(member and member.role in {TeamMember.Role.OWNER, TeamMember.Role.ADMIN})
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def team_members(request):
|
||||
team = get_current_team(request.user)
|
||||
if request.method == "GET":
|
||||
members = TeamMember.objects.filter(team=team).select_related("user").order_by("created_at")
|
||||
return Response(TeamMemberSerializer(members, many=True).data)
|
||||
|
||||
if not can_manage_team(request.user, team):
|
||||
return Response({"detail": "permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
username = str(request.data.get("username") or "").strip()
|
||||
password = str(request.data.get("password") or "").strip()
|
||||
if not username:
|
||||
return Response({"username": ["This field is required."]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if len(password) < 8:
|
||||
return Response({"password": ["Ensure this field has at least 8 characters."]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if User.objects.filter(username=username).exists():
|
||||
return Response({"username": ["username already exists"]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
email = str(request.data.get("email") or "").strip() or f"{username}@airshelf.local"
|
||||
role = normalize_member_role(request.data.get("role"))
|
||||
if role == TeamMember.Role.OWNER:
|
||||
role = TeamMember.Role.ADMIN
|
||||
with transaction.atomic():
|
||||
user = User.objects.create_user(username=username, password=password, email=email)
|
||||
user.first_name = str(request.data.get("name") or "").strip()
|
||||
user.save(update_fields=["first_name"])
|
||||
member = TeamMember.objects.create(
|
||||
team=team,
|
||||
user=user,
|
||||
role=role,
|
||||
monthly_credit_limit=request.data.get("monthly_credit_limit") or request.data.get("monthly") or 0,
|
||||
)
|
||||
return Response(TeamMemberSerializer(member).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["PATCH", "DELETE"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def team_member_detail(request, member_id):
|
||||
team = get_current_team(request.user)
|
||||
if not can_manage_team(request.user, team):
|
||||
return Response({"detail": "permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
member = TeamMember.objects.select_related("user").filter(team=team, id=member_id).first()
|
||||
if member is None:
|
||||
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
if member.user_id == team.owner_id:
|
||||
return Response({"detail": "team owner cannot be changed"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
if request.method == "DELETE":
|
||||
user = member.user
|
||||
member.delete()
|
||||
if not TeamMember.objects.filter(user=user).exists():
|
||||
user.status = User.Status.DISABLED
|
||||
user.save(update_fields=["status"])
|
||||
Token.objects.filter(user=user).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
role = request.data.get("role")
|
||||
if role:
|
||||
member.role = normalize_member_role(role)
|
||||
if member.role == TeamMember.Role.OWNER:
|
||||
member.role = TeamMember.Role.ADMIN
|
||||
if "monthly_credit_limit" in request.data or "monthly" in request.data:
|
||||
member.monthly_credit_limit = request.data.get("monthly_credit_limit", request.data.get("monthly")) or 0
|
||||
name = str(request.data.get("name") or "").strip()
|
||||
if name:
|
||||
member.user.first_name = name
|
||||
member.user.save(update_fields=["first_name"])
|
||||
member.save(update_fields=["role", "monthly_credit_limit", "updated_at"])
|
||||
return Response(TeamMemberSerializer(member).data)
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def team_member_password(request, member_id):
|
||||
team = get_current_team(request.user)
|
||||
if not can_manage_team(request.user, team):
|
||||
return Response({"detail": "permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
member = TeamMember.objects.select_related("user").filter(team=team, id=member_id).first()
|
||||
if member is None:
|
||||
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
if member.user_id == team.owner_id:
|
||||
return Response({"detail": "team owner password cannot be reset here"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
password = str(request.data.get("password") or "").strip()
|
||||
if len(password) < 8:
|
||||
return Response({"password": ["Ensure this field has at least 8 characters."]}, status=status.HTTP_400_BAD_REQUEST)
|
||||
member.user.set_password(password)
|
||||
member.user.save(update_fields=["password"])
|
||||
Token.objects.filter(user=member.user).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
Reference in New Issue
Block a user