BmorricalDjangoWorkflows 2.0.4__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/BmorricalDjangoWorkflows.egg-info/PKG-INFO +5 -1
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/BmorricalDjangoWorkflows.egg-info/SOURCES.txt +4 -1
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/PKG-INFO +5 -1
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/README.md +4 -0
- bmorricaldjangoworkflows-2.1.0/VERSION +1 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/users/services/auth_flow.py +193 -1
- bmorricaldjangoworkflows-2.1.0/django_workflows/users/urls.py +19 -0
- bmorricaldjangoworkflows-2.1.0/django_workflows/users/views.py +189 -0
- bmorricaldjangoworkflows-2.1.0/tests/test_auth_flow.py +1259 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/tests/test_email.py +67 -0
- bmorricaldjangoworkflows-2.1.0/tests/test_user_views.py +358 -0
- bmorricaldjangoworkflows-2.0.4/VERSION +0 -1
- bmorricaldjangoworkflows-2.0.4/tests/test_auth_flow.py +0 -537
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/BmorricalDjangoWorkflows.egg-info/dependency_links.txt +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/BmorricalDjangoWorkflows.egg-info/requires.txt +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/BmorricalDjangoWorkflows.egg-info/top_level.txt +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/LICENSE +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/MANIFEST.in +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/services/email.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/users/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/django_workflows/users/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/setup.cfg +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/setup.py +0 -0
- {bmorricaldjangoworkflows-2.0.4 → bmorricaldjangoworkflows-2.1.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BmorricalDjangoWorkflows
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Reusable Django Workflows
|
|
5
5
|
Home-page: https://github.com/Bmorrical/django-workflows
|
|
6
6
|
Author: Bradley Morrical
|
|
@@ -308,6 +308,10 @@ This is expected unless you set `BCC_RECIPIENTS`.
|
|
|
308
308
|
|
|
309
309
|
## Release
|
|
310
310
|
|
|
311
|
+
Update code and ./VERSION
|
|
312
|
+
|
|
313
|
+
Commit changes
|
|
314
|
+
|
|
311
315
|
Create a release tag:
|
|
312
316
|
|
|
313
317
|
```bash
|
|
@@ -12,8 +12,11 @@ django_workflows/__init__.py
|
|
|
12
12
|
django_workflows/services/__init__.py
|
|
13
13
|
django_workflows/services/email.py
|
|
14
14
|
django_workflows/users/__init__.py
|
|
15
|
+
django_workflows/users/urls.py
|
|
16
|
+
django_workflows/users/views.py
|
|
15
17
|
django_workflows/users/services/__init__.py
|
|
16
18
|
django_workflows/users/services/auth_flow.py
|
|
17
19
|
tests/__init__.py
|
|
18
20
|
tests/test_auth_flow.py
|
|
19
|
-
tests/test_email.py
|
|
21
|
+
tests/test_email.py
|
|
22
|
+
tests/test_user_views.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BmorricalDjangoWorkflows
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Reusable Django Workflows
|
|
5
5
|
Home-page: https://github.com/Bmorrical/django-workflows
|
|
6
6
|
Author: Bradley Morrical
|
|
@@ -308,6 +308,10 @@ This is expected unless you set `BCC_RECIPIENTS`.
|
|
|
308
308
|
|
|
309
309
|
## Release
|
|
310
310
|
|
|
311
|
+
Update code and ./VERSION
|
|
312
|
+
|
|
313
|
+
Commit changes
|
|
314
|
+
|
|
311
315
|
Create a release tag:
|
|
312
316
|
|
|
313
317
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.0
|
|
@@ -149,6 +149,120 @@ def _get_user_meta_model() -> Type[Any]:
|
|
|
149
149
|
f"Unable to import WORKFLOWS['USER_META_MODEL']: {model_path!r}"
|
|
150
150
|
) from exc
|
|
151
151
|
|
|
152
|
+
|
|
153
|
+
def _get_group_model() -> Type[Any]:
|
|
154
|
+
from django.contrib.auth.models import Group
|
|
155
|
+
|
|
156
|
+
return Group
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normalize_group_names(value: Any) -> list[str]:
|
|
160
|
+
if value is None:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
if isinstance(value, str):
|
|
164
|
+
candidates = value.split(",")
|
|
165
|
+
elif isinstance(value, (list, tuple, set)):
|
|
166
|
+
candidates = []
|
|
167
|
+
for item in value:
|
|
168
|
+
if isinstance(item, str):
|
|
169
|
+
candidates.extend(item.split(","))
|
|
170
|
+
elif item is not None:
|
|
171
|
+
candidates.append(str(item))
|
|
172
|
+
else:
|
|
173
|
+
candidates = [str(value)]
|
|
174
|
+
|
|
175
|
+
normalized: list[str] = []
|
|
176
|
+
seen: set[str] = set()
|
|
177
|
+
for candidate in candidates:
|
|
178
|
+
name = candidate.strip()
|
|
179
|
+
if name and name not in seen:
|
|
180
|
+
seen.add(name)
|
|
181
|
+
normalized.append(name)
|
|
182
|
+
|
|
183
|
+
return normalized
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _get_existing_groups(group_names: Any) -> list[Any]:
|
|
187
|
+
normalized_names = _normalize_group_names(group_names)
|
|
188
|
+
if not normalized_names:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
Group = _get_group_model()
|
|
192
|
+
return list(Group.objects.filter(name__in=normalized_names))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _add_existing_groups_to_user(*, user: Any, group_names: Any) -> list[str]:
|
|
196
|
+
groups = _get_existing_groups(group_names)
|
|
197
|
+
group_manager = getattr(user, "groups", None)
|
|
198
|
+
|
|
199
|
+
if not groups or group_manager is None or not hasattr(group_manager, "add"):
|
|
200
|
+
return []
|
|
201
|
+
|
|
202
|
+
group_manager.add(*groups)
|
|
203
|
+
return [group.name for group in groups]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _coerce_bool(value: Any) -> bool:
|
|
207
|
+
if isinstance(value, bool):
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
if isinstance(value, str):
|
|
211
|
+
normalized = value.strip().lower()
|
|
212
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
213
|
+
return True
|
|
214
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
raise ValueError("Boolean value required.")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _get_force_password_reset(user: Any) -> bool:
|
|
221
|
+
UserMeta = _get_user_meta_model()
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
meta = UserMeta.objects.get(user=user)
|
|
225
|
+
return bool(getattr(meta, "force_password_reset", False))
|
|
226
|
+
except UserMeta.DoesNotExist:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_user_details(user: Any) -> dict[str, Any]:
|
|
231
|
+
return {
|
|
232
|
+
"id": user.id,
|
|
233
|
+
"username": user.username,
|
|
234
|
+
"email": user.email,
|
|
235
|
+
"first_name": user.first_name,
|
|
236
|
+
"last_name": user.last_name,
|
|
237
|
+
"is_active": bool(getattr(user, "is_active", False)),
|
|
238
|
+
"force_password_reset": _get_force_password_reset(user),
|
|
239
|
+
"groups": [group.name for group in user.groups.all()],
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def list_users(*, exclude_groups: Any = None) -> list[dict[str, Any]]:
|
|
244
|
+
User = get_user_model()
|
|
245
|
+
excluded_group_names = set(_normalize_group_names(exclude_groups))
|
|
246
|
+
users = list(User.objects.all())
|
|
247
|
+
users.sort(key=lambda user: getattr(user, "id", 0))
|
|
248
|
+
|
|
249
|
+
if excluded_group_names:
|
|
250
|
+
filtered_users = []
|
|
251
|
+
for user in users:
|
|
252
|
+
user_group_names = {group.name for group in user.groups.all()}
|
|
253
|
+
if user_group_names.intersection(excluded_group_names):
|
|
254
|
+
continue
|
|
255
|
+
filtered_users.append(user)
|
|
256
|
+
users = filtered_users
|
|
257
|
+
|
|
258
|
+
return [get_user_details(user) for user in users]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True)
|
|
262
|
+
class UserUpdateResult:
|
|
263
|
+
success: bool
|
|
264
|
+
message: str
|
|
265
|
+
|
|
152
266
|
class VerifyCodeResult(Enum):
|
|
153
267
|
SUCCESS = "success"
|
|
154
268
|
INVALID = "invalid"
|
|
@@ -177,7 +291,14 @@ class ChangePasswordResult:
|
|
|
177
291
|
message: str
|
|
178
292
|
|
|
179
293
|
|
|
180
|
-
def register_user_and_send_verification_email(
|
|
294
|
+
def register_user_and_send_verification_email(
|
|
295
|
+
email: str,
|
|
296
|
+
first_name: str,
|
|
297
|
+
last_name: str,
|
|
298
|
+
password: str,
|
|
299
|
+
group: Any = None,
|
|
300
|
+
groups: Any = None,
|
|
301
|
+
) -> ServiceResult:
|
|
181
302
|
"""
|
|
182
303
|
Registers a new user and sends a verification email.
|
|
183
304
|
"""
|
|
@@ -219,6 +340,13 @@ def register_user_and_send_verification_email(email: str, first_name: str, last_
|
|
|
219
340
|
meta.verify_time = timezone.now()
|
|
220
341
|
meta.save()
|
|
221
342
|
|
|
343
|
+
requested_groups = _normalize_group_names(groups)
|
|
344
|
+
if group is not None:
|
|
345
|
+
requested_groups.extend(
|
|
346
|
+
name for name in _normalize_group_names(group) if name not in requested_groups
|
|
347
|
+
)
|
|
348
|
+
_add_existing_groups_to_user(user=user, group_names=requested_groups)
|
|
349
|
+
|
|
222
350
|
message = (
|
|
223
351
|
f"Hello {first_name} {last_name},<br><br>"
|
|
224
352
|
f"Your account verification code is: <strong>{verification_code}</strong><br /><br />"
|
|
@@ -273,6 +401,70 @@ def register_user_and_send_verification_email(email: str, first_name: str, last_
|
|
|
273
401
|
message="Registration successful. Please check your email for the verification code.",
|
|
274
402
|
)
|
|
275
403
|
|
|
404
|
+
|
|
405
|
+
def update_user(user: Any, data: dict[str, Any]) -> UserUpdateResult:
|
|
406
|
+
processed_update = False
|
|
407
|
+
user_fields_updated = False
|
|
408
|
+
|
|
409
|
+
email_value = data.get("email")
|
|
410
|
+
username_value = data.get("username")
|
|
411
|
+
if email_value is not None or username_value is not None:
|
|
412
|
+
next_email = email_value if email_value is not None else username_value
|
|
413
|
+
user.username = next_email
|
|
414
|
+
user.email = next_email
|
|
415
|
+
processed_update = True
|
|
416
|
+
user_fields_updated = True
|
|
417
|
+
|
|
418
|
+
if "first_name" in data:
|
|
419
|
+
user.first_name = data["first_name"]
|
|
420
|
+
processed_update = True
|
|
421
|
+
user_fields_updated = True
|
|
422
|
+
if "last_name" in data:
|
|
423
|
+
user.last_name = data["last_name"]
|
|
424
|
+
processed_update = True
|
|
425
|
+
user_fields_updated = True
|
|
426
|
+
if "password" in data:
|
|
427
|
+
user.set_password(data["password"])
|
|
428
|
+
processed_update = True
|
|
429
|
+
user_fields_updated = True
|
|
430
|
+
if "is_active" in data:
|
|
431
|
+
try:
|
|
432
|
+
user.is_active = _coerce_bool(data["is_active"])
|
|
433
|
+
except ValueError:
|
|
434
|
+
return UserUpdateResult(
|
|
435
|
+
success=False,
|
|
436
|
+
message="is_active must be a boolean value.",
|
|
437
|
+
)
|
|
438
|
+
processed_update = True
|
|
439
|
+
user_fields_updated = True
|
|
440
|
+
|
|
441
|
+
requested_groups = []
|
|
442
|
+
if "groups" in data:
|
|
443
|
+
requested_groups.extend(_normalize_group_names(data.get("groups")))
|
|
444
|
+
processed_update = True
|
|
445
|
+
if "group" in data:
|
|
446
|
+
requested_groups.extend(
|
|
447
|
+
name for name in _normalize_group_names(data.get("group")) if name not in requested_groups
|
|
448
|
+
)
|
|
449
|
+
processed_update = True
|
|
450
|
+
if requested_groups:
|
|
451
|
+
_add_existing_groups_to_user(user=user, group_names=requested_groups)
|
|
452
|
+
|
|
453
|
+
if user_fields_updated:
|
|
454
|
+
user.save()
|
|
455
|
+
|
|
456
|
+
if not processed_update:
|
|
457
|
+
return UserUpdateResult(
|
|
458
|
+
success=False,
|
|
459
|
+
message="No valid fields provided.",
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
logger.info("User updated for %s", user.email)
|
|
463
|
+
return UserUpdateResult(
|
|
464
|
+
success=True,
|
|
465
|
+
message="User updated successfully.",
|
|
466
|
+
)
|
|
467
|
+
|
|
276
468
|
def send_forgot_password_code(email: str) -> ServiceResult:
|
|
277
469
|
"""
|
|
278
470
|
Generates and sends a password reset code.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from django_workflows.users.views import (
|
|
4
|
+
ChangePasswordView,
|
|
5
|
+
ForgotPasswordView,
|
|
6
|
+
UserCreateView,
|
|
7
|
+
UserDetailView,
|
|
8
|
+
UserListView,
|
|
9
|
+
VerifyCodeView,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
urlpatterns = [
|
|
13
|
+
path("user/", UserDetailView.as_view(), name="user-detail"),
|
|
14
|
+
path("users/", UserListView.as_view(), name="user-list"),
|
|
15
|
+
path("forgot-password/", ForgotPasswordView.as_view(), name="forgot-password"),
|
|
16
|
+
path("verify-code/", VerifyCodeView.as_view(), name="verify-code"),
|
|
17
|
+
path("change-password/", ChangePasswordView.as_view(), name="change-password"),
|
|
18
|
+
path("register/", UserCreateView.as_view(), name="register"),
|
|
19
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from rest_framework import status
|
|
4
|
+
from rest_framework.permissions import IsAuthenticated
|
|
5
|
+
from rest_framework.response import Response
|
|
6
|
+
from rest_framework.views import APIView
|
|
7
|
+
|
|
8
|
+
from django_workflows.users.services.auth_flow import (
|
|
9
|
+
change_password,
|
|
10
|
+
get_user_details,
|
|
11
|
+
list_users,
|
|
12
|
+
register_user_and_send_verification_email,
|
|
13
|
+
send_forgot_password_code,
|
|
14
|
+
update_user,
|
|
15
|
+
verify_reset_code,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AnonymousWorkflowView(APIView):
|
|
22
|
+
authentication_classes = []
|
|
23
|
+
permission_classes = []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Route: POST /forgot-password/
|
|
27
|
+
# Body params: email, optional company honeypot field
|
|
28
|
+
class ForgotPasswordView(AnonymousWorkflowView):
|
|
29
|
+
def post(self, request):
|
|
30
|
+
data = request.data
|
|
31
|
+
|
|
32
|
+
if data.get("company") is not None and str(data.get("company")).strip():
|
|
33
|
+
logger.warning(
|
|
34
|
+
"Honeypot triggered for password reset: company=%s",
|
|
35
|
+
data.get("company"),
|
|
36
|
+
)
|
|
37
|
+
return Response(
|
|
38
|
+
{"detail": "Password reset blocked."},
|
|
39
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
email = data.get("email")
|
|
43
|
+
if not email:
|
|
44
|
+
logger.error("ForgotPasswordView: Email is required.")
|
|
45
|
+
return Response(
|
|
46
|
+
{"detail": "Email is required."},
|
|
47
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
result = send_forgot_password_code(email)
|
|
51
|
+
return Response({"detail": result.message}, status=status.HTTP_200_OK)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Route: POST /register/
|
|
55
|
+
# Body params: firstName, lastName, email, password, optional group, optional groups, optional company honeypot field
|
|
56
|
+
class UserCreateView(AnonymousWorkflowView):
|
|
57
|
+
def post(self, request):
|
|
58
|
+
data = request.data
|
|
59
|
+
|
|
60
|
+
if data.get("company") is not None and str(data.get("company")).strip():
|
|
61
|
+
logger.warning(
|
|
62
|
+
"Honeypot triggered for registration: company=%s",
|
|
63
|
+
data.get("company"),
|
|
64
|
+
)
|
|
65
|
+
return Response(
|
|
66
|
+
{"detail": "Bot detected. Registration blocked."},
|
|
67
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
required_fields = ["firstName", "lastName", "password", "email"]
|
|
71
|
+
missing_fields = [field for field in required_fields if field not in data]
|
|
72
|
+
if missing_fields:
|
|
73
|
+
logger.error("Registration missing fields: %s", missing_fields)
|
|
74
|
+
return Response(
|
|
75
|
+
{"detail": f"Missing required field(s): {', '.join(missing_fields)}"},
|
|
76
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
result = register_user_and_send_verification_email(
|
|
80
|
+
first_name=data.get("firstName"),
|
|
81
|
+
last_name=data.get("lastName"),
|
|
82
|
+
email=data.get("email"),
|
|
83
|
+
password=data.get("password"),
|
|
84
|
+
group=data.get("group"),
|
|
85
|
+
groups=data.get("groups"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not result.success:
|
|
89
|
+
response_status = status.HTTP_409_CONFLICT
|
|
90
|
+
if result.message != "A user with this email already exists.":
|
|
91
|
+
response_status = status.HTTP_400_BAD_REQUEST
|
|
92
|
+
|
|
93
|
+
return Response({"detail": result.message}, status=response_status)
|
|
94
|
+
|
|
95
|
+
return Response({"detail": result.message}, status=status.HTTP_201_CREATED)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Route: POST /verify-code/
|
|
99
|
+
# Body params: email, code, optional company honeypot field
|
|
100
|
+
class VerifyCodeView(AnonymousWorkflowView):
|
|
101
|
+
def post(self, request):
|
|
102
|
+
data = request.data
|
|
103
|
+
|
|
104
|
+
if data.get("company") is not None and str(data.get("company")).strip():
|
|
105
|
+
logger.warning(
|
|
106
|
+
"Honeypot triggered for verification: company=%s",
|
|
107
|
+
data.get("company"),
|
|
108
|
+
)
|
|
109
|
+
return Response(
|
|
110
|
+
{"detail": "Verification blocked."},
|
|
111
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
email = data.get("email")
|
|
115
|
+
code = data.get("code")
|
|
116
|
+
if not email or not code:
|
|
117
|
+
return Response(
|
|
118
|
+
{"detail": "Email and code are required."},
|
|
119
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result = verify_reset_code(email=email, code=code)
|
|
123
|
+
return Response(result.payload, status=result.status_code)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Route: POST /change-password/
|
|
127
|
+
# Body params: email, password, passwordVerify, optional company honeypot field
|
|
128
|
+
class ChangePasswordView(AnonymousWorkflowView):
|
|
129
|
+
def post(self, request):
|
|
130
|
+
data = request.data
|
|
131
|
+
|
|
132
|
+
if data.get("company") is not None and str(data.get("company")).strip():
|
|
133
|
+
logger.warning(
|
|
134
|
+
"Honeypot triggered for change password: company=%s",
|
|
135
|
+
data.get("company"),
|
|
136
|
+
)
|
|
137
|
+
return Response(
|
|
138
|
+
{"detail": "Bot detected. Change password blocked."},
|
|
139
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
result = change_password(
|
|
143
|
+
email=data.get("email"),
|
|
144
|
+
password=data.get("password"),
|
|
145
|
+
password_verify=data.get("passwordVerify"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not result.success:
|
|
149
|
+
return Response(
|
|
150
|
+
{"detail": result.message},
|
|
151
|
+
status=status.HTTP_400_BAD_REQUEST,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return Response({"detail": result.message}, status=status.HTTP_200_OK)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# Route: GET /users/
|
|
158
|
+
# Query params: optional exclude_group as comma-separated group names
|
|
159
|
+
class UserListView(APIView):
|
|
160
|
+
permission_classes = [IsAuthenticated]
|
|
161
|
+
|
|
162
|
+
def get(self, request):
|
|
163
|
+
users = list_users(exclude_groups=request.query_params.get("exclude_group"))
|
|
164
|
+
logger.info("UserListView: Fetched %s users", len(users))
|
|
165
|
+
return Response(users, status=status.HTTP_200_OK)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Route: GET /user/
|
|
169
|
+
# Route: PATCH /user/
|
|
170
|
+
# PATCH body params: username, email, first_name, last_name, password, is_active, optional group, optional groups
|
|
171
|
+
class UserDetailView(APIView):
|
|
172
|
+
permission_classes = [IsAuthenticated]
|
|
173
|
+
|
|
174
|
+
def get(self, request):
|
|
175
|
+
data = get_user_details(request.user)
|
|
176
|
+
logger.info("UserDetailView: Fetched details for user %s", request.user.email)
|
|
177
|
+
return Response(data, status=status.HTTP_200_OK)
|
|
178
|
+
|
|
179
|
+
def patch(self, request):
|
|
180
|
+
result = update_user(request.user, request.data)
|
|
181
|
+
|
|
182
|
+
if result.success:
|
|
183
|
+
return Response(
|
|
184
|
+
{"detail": result.message},
|
|
185
|
+
status=status.HTTP_200_OK,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
logger.error("UserDetailView: %s for user %s", result.message, request.user.email)
|
|
189
|
+
return Response({"detail": result.message}, status=status.HTTP_400_BAD_REQUEST)
|