1
|
|
|
""" |
2
|
|
|
byceps.services.user.user_domain_service |
3
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
4
|
|
|
|
5
|
|
|
:Copyright: 2014-2025 Jochen Kupperschmidt |
6
|
|
|
:License: Revised BSD (see `LICENSE` file for details) |
7
|
|
|
""" |
8
|
|
|
|
9
|
1 |
|
from datetime import date, datetime |
10
|
|
|
from typing import Any |
11
|
1 |
|
|
12
|
1 |
|
from byceps.services.core.events import EventUser |
13
|
|
|
from byceps.util.result import Err, Ok, Result |
14
|
1 |
|
from byceps.util.uuid import generate_uuid7 |
15
|
|
|
|
16
|
|
|
from .errors import NothingChangedError |
17
|
|
|
from .events import ( |
18
|
|
|
UserAccountDeletedEvent, |
19
|
|
|
UserAccountSuspendedEvent, |
20
|
|
|
UserAccountUnsuspendedEvent, |
21
|
|
|
UserDetailsUpdatedEvent, |
22
|
|
|
UserScreenNameChangedEvent, |
23
|
|
|
) |
24
|
|
|
from .models.log import UserLogEntry, UserLogEntryData |
25
|
1 |
|
from .models.user import User |
26
|
1 |
|
|
27
|
1 |
|
|
28
|
|
|
def suspend_account( |
29
|
1 |
|
user: User, initiator: User, reason: str |
30
|
|
|
) -> tuple[UserAccountSuspendedEvent, UserLogEntry]: |
31
|
|
|
"""Suspend the user account.""" |
32
|
|
|
occurred_at = datetime.utcnow() |
33
|
|
|
|
34
|
1 |
|
event = _build_account_suspended_event(occurred_at, initiator, user) |
35
|
1 |
|
|
36
|
|
|
log_entry = _build_account_suspended_log_entry( |
37
|
|
|
occurred_at, initiator, user, reason |
38
|
1 |
|
) |
39
|
|
|
|
40
|
|
|
return event, log_entry |
41
|
|
|
|
42
|
|
|
|
43
|
|
|
def _build_account_suspended_event( |
44
|
|
|
occurred_at: datetime, initiator: User, user: User |
45
|
|
|
) -> UserAccountSuspendedEvent: |
46
|
|
|
return UserAccountSuspendedEvent( |
47
|
|
|
occurred_at=occurred_at, |
48
|
|
|
initiator=EventUser.from_user(initiator), |
49
|
|
|
user=EventUser.from_user(user), |
50
|
|
|
) |
51
|
|
|
|
52
|
|
|
|
53
|
|
|
def _build_account_suspended_log_entry( |
54
|
1 |
|
occurred_at: datetime, initiator: User, user: User, reason: str |
55
|
1 |
|
) -> UserLogEntry: |
56
|
|
|
return UserLogEntry( |
57
|
|
|
id=generate_uuid7(), |
58
|
1 |
|
occurred_at=occurred_at, |
59
|
1 |
|
event_type='user-suspended', |
60
|
|
|
user_id=user.id, |
61
|
1 |
|
initiator_id=initiator.id, |
62
|
|
|
data={ |
63
|
|
|
'initiator_id': str(initiator.id), |
64
|
1 |
|
'reason': reason, |
65
|
|
|
}, |
66
|
1 |
|
) |
67
|
|
|
|
68
|
|
|
|
69
|
1 |
|
def unsuspend_account( |
70
|
1 |
|
user: User, initiator: User, reason: str |
71
|
|
|
) -> tuple[UserAccountUnsuspendedEvent, UserLogEntry]: |
72
|
|
|
"""Unsuspend the user account.""" |
73
|
|
|
occurred_at = datetime.utcnow() |
74
|
1 |
|
|
75
|
|
|
event = _build_account_unsuspended_event(occurred_at, initiator, user) |
76
|
|
|
|
77
|
1 |
|
log_entry = _build_account_unsuspended_log_entry( |
78
|
|
|
occurred_at, initiator, user, reason |
79
|
|
|
) |
80
|
|
|
|
81
|
1 |
|
return event, log_entry |
82
|
|
|
|
83
|
|
|
|
84
|
|
|
def _build_account_unsuspended_event( |
85
|
|
|
occurred_at: datetime, initiator: User, user: User |
86
|
|
|
) -> UserAccountUnsuspendedEvent: |
87
|
|
|
return UserAccountUnsuspendedEvent( |
88
|
|
|
occurred_at=occurred_at, |
89
|
|
|
initiator=EventUser.from_user(initiator), |
90
|
|
|
user=EventUser.from_user(user), |
91
|
1 |
|
) |
92
|
|
|
|
93
|
|
|
|
94
|
|
|
def _build_account_unsuspended_log_entry( |
95
|
1 |
|
occurred_at: datetime, initiator: User, user: User, reason: str |
96
|
|
|
) -> UserLogEntry: |
97
|
|
|
return UserLogEntry( |
98
|
|
|
id=generate_uuid7(), |
99
|
1 |
|
occurred_at=occurred_at, |
100
|
|
|
event_type='user-unsuspended', |
101
|
|
|
user_id=user.id, |
102
|
1 |
|
initiator_id=initiator.id, |
103
|
|
|
data={ |
104
|
|
|
'initiator_id': str(initiator.id), |
105
|
|
|
'reason': reason, |
106
|
|
|
}, |
107
|
|
|
) |
108
|
|
|
|
109
|
1 |
|
|
110
|
|
|
def delete_account( |
111
|
|
|
user: User, |
112
|
|
|
initiator: User, |
113
|
|
|
reason: str, |
114
|
|
|
) -> tuple[UserAccountDeletedEvent, UserLogEntry]: |
115
|
|
|
"""Delete the user account.""" |
116
|
|
|
occurred_at = datetime.utcnow() |
117
|
|
|
|
118
|
|
|
event = _build_account_deleted_event(occurred_at, initiator, user) |
119
|
|
|
|
120
|
1 |
|
log_entry = _build_account_deleted_log_entry( |
121
|
|
|
occurred_at, initiator, user, reason |
122
|
|
|
) |
123
|
|
|
|
124
|
|
|
return event, log_entry |
125
|
|
|
|
126
|
|
|
|
127
|
|
|
def _build_account_deleted_event( |
128
|
1 |
|
occurred_at: datetime, |
129
|
|
|
initiator: User, |
130
|
1 |
|
user: User, |
131
|
|
|
) -> UserAccountDeletedEvent: |
132
|
|
|
return UserAccountDeletedEvent( |
133
|
1 |
|
occurred_at=occurred_at, |
134
|
1 |
|
initiator=EventUser.from_user(initiator), |
135
|
|
|
user=EventUser.from_user(user), |
136
|
1 |
|
) |
137
|
1 |
|
|
138
|
|
|
|
139
|
1 |
|
def _build_account_deleted_log_entry( |
140
|
1 |
|
occurred_at: datetime, |
141
|
|
|
initiator: User, |
142
|
1 |
|
user: User, |
143
|
|
|
reason: str, |
144
|
|
|
) -> UserLogEntry: |
145
|
|
|
return UserLogEntry( |
146
|
|
|
id=generate_uuid7(), |
147
|
|
|
occurred_at=occurred_at, |
148
|
|
|
event_type='user-deleted', |
149
|
|
|
user_id=user.id, |
150
|
|
|
initiator_id=initiator.id, |
151
|
|
|
data={ |
152
|
1 |
|
'initiator_id': str(initiator.id), |
153
|
|
|
'reason': reason, |
154
|
|
|
}, |
155
|
|
|
) |
156
|
|
|
|
157
|
|
|
|
158
|
1 |
|
def change_screen_name( |
159
|
1 |
|
user: User, |
160
|
|
|
new_screen_name: str, |
161
|
1 |
|
initiator: User, |
162
|
|
|
*, |
163
|
1 |
|
reason: str | None = None, |
164
|
|
|
) -> tuple[UserScreenNameChangedEvent, UserLogEntry]: |
165
|
|
|
"""Change the user's screen name.""" |
166
|
|
|
occurred_at = datetime.utcnow() |
167
|
1 |
|
old_screen_name = user.screen_name |
168
|
|
|
|
169
|
|
|
event = _build_screen_name_changed_event( |
170
|
1 |
|
occurred_at, initiator, user, old_screen_name, new_screen_name |
171
|
|
|
) |
172
|
|
|
|
173
|
1 |
|
log_entry = _build_screen_name_changed_log_entry( |
174
|
|
|
occurred_at, initiator, user, old_screen_name, new_screen_name, reason |
175
|
1 |
|
) |
176
|
1 |
|
|
177
|
|
|
return event, log_entry |
178
|
1 |
|
|
179
|
|
|
|
180
|
|
|
def _build_screen_name_changed_event( |
181
|
|
|
occurred_at: datetime, |
182
|
|
|
initiator: User, |
183
|
|
|
user: User, |
184
|
|
|
old_screen_name: str | None, |
185
|
|
|
new_screen_name: str | None, |
186
|
|
|
) -> UserScreenNameChangedEvent: |
187
|
|
|
return UserScreenNameChangedEvent( |
188
|
1 |
|
occurred_at=occurred_at, |
189
|
|
|
initiator=EventUser.from_user(initiator), |
190
|
|
|
user_id=user.id, |
191
|
|
|
old_screen_name=old_screen_name, |
192
|
1 |
|
new_screen_name=new_screen_name, |
193
|
|
|
) |
194
|
1 |
|
|
195
|
|
|
|
196
|
1 |
|
def _build_screen_name_changed_log_entry( |
197
|
|
|
occurred_at: datetime, |
198
|
|
|
initiator: User, |
199
|
|
|
user: User, |
200
|
1 |
|
old_screen_name: str | None, |
201
|
|
|
new_screen_name: str | None, |
202
|
|
|
reason: str | None, |
203
|
1 |
|
) -> UserLogEntry: |
204
|
|
|
data = { |
205
|
|
|
'initiator_id': str(initiator.id), |
206
|
1 |
|
'old_screen_name': old_screen_name, |
207
|
|
|
'new_screen_name': new_screen_name, |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
if reason: |
211
|
|
|
data['reason'] = reason |
212
|
|
|
|
213
|
|
|
return UserLogEntry( |
214
|
|
|
id=generate_uuid7(), |
215
|
1 |
|
occurred_at=occurred_at, |
216
|
|
|
event_type='user-screen-name-changed', |
217
|
|
|
user_id=user.id, |
218
|
1 |
|
initiator_id=initiator.id, |
219
|
|
|
data=data, |
220
|
|
|
) |
221
|
|
|
|
222
|
|
|
|
223
|
|
|
def update_details( |
224
|
|
|
user: User, |
225
|
|
|
old_first_name: str | None, |
226
|
|
|
new_first_name: str | None, |
227
|
|
|
old_last_name: str | None, |
228
|
|
|
new_last_name: str | None, |
229
|
|
|
old_date_of_birth: date | None, |
230
|
|
|
new_date_of_birth: date | None, |
231
|
1 |
|
old_country: str | None, |
232
|
|
|
new_country: str | None, |
233
|
|
|
old_postal_code: str | None, |
234
|
|
|
new_postal_code: str | None, |
235
|
1 |
|
old_city: str | None, |
236
|
|
|
new_city: str | None, |
237
|
1 |
|
old_street: str | None, |
238
|
|
|
new_street: str | None, |
239
|
1 |
|
old_phone_number: str | None, |
240
|
|
|
new_phone_number: str | None, |
241
|
|
|
initiator: User, |
242
|
|
|
) -> Result[tuple[UserDetailsUpdatedEvent, UserLogEntry], NothingChangedError]: |
243
|
1 |
|
"""Update the user's details.""" |
244
|
|
|
occurred_at = datetime.utcnow() |
245
|
|
|
|
246
|
1 |
|
event = _build_details_updated_event(occurred_at, initiator, user) |
247
|
|
|
|
248
|
|
|
log_entry_result = _build_details_updated_log_entry( |
249
|
1 |
|
occurred_at, |
250
|
|
|
initiator, |
251
|
|
|
user, |
252
|
|
|
old_first_name, |
253
|
|
|
new_first_name, |
254
|
|
|
old_last_name, |
255
|
|
|
new_last_name, |
256
|
|
|
old_date_of_birth, |
257
|
|
|
new_date_of_birth, |
258
|
1 |
|
old_country, |
259
|
|
|
new_country, |
260
|
|
|
old_postal_code, |
261
|
1 |
|
new_postal_code, |
262
|
|
|
old_city, |
263
|
|
|
new_city, |
264
|
|
|
old_street, |
265
|
|
|
new_street, |
266
|
|
|
old_phone_number, |
267
|
|
|
new_phone_number, |
268
|
|
|
) |
269
|
|
|
if log_entry_result.is_err(): |
270
|
|
|
return Err(log_entry_result.unwrap_err()) |
271
|
|
|
|
272
|
|
|
log_entry = log_entry_result.unwrap() |
273
|
|
|
|
274
|
1 |
|
return Ok((event, log_entry)) |
275
|
|
|
|
276
|
|
|
|
277
|
|
|
def _build_details_updated_event( |
278
|
|
|
occurred_at: datetime, |
279
|
|
|
initiator: User, |
280
|
1 |
|
user: User, |
281
|
|
|
) -> UserDetailsUpdatedEvent: |
282
|
1 |
|
return UserDetailsUpdatedEvent( |
283
|
|
|
occurred_at=occurred_at, |
284
|
1 |
|
initiator=EventUser.from_user(initiator), |
285
|
|
|
user=EventUser.from_user(user), |
286
|
|
|
) |
287
|
|
|
|
288
|
1 |
|
|
289
|
|
|
def _build_details_updated_log_entry( |
290
|
|
|
occurred_at: datetime, |
291
|
1 |
|
initiator: User, |
292
|
|
|
user: User, |
293
|
|
|
old_first_name: str | None, |
294
|
|
|
new_first_name: str | None, |
295
|
|
|
old_last_name: str | None, |
296
|
1 |
|
new_last_name: str | None, |
297
|
|
|
old_date_of_birth: date | None, |
298
|
|
|
new_date_of_birth: date | None, |
299
|
|
|
old_country: str | None, |
300
|
|
|
new_country: str | None, |
301
|
|
|
old_postal_code: str | None, |
302
|
|
|
new_postal_code: str | None, |
303
|
|
|
old_city: str | None, |
304
|
|
|
new_city: str | None, |
305
|
1 |
|
old_street: str | None, |
306
|
|
|
new_street: str | None, |
307
|
|
|
old_phone_number: str | None, |
308
|
|
|
new_phone_number: str | None, |
309
|
|
|
) -> Result[UserLogEntry, NothingChangedError]: |
310
|
|
|
fields: dict[str, str] = {} |
311
|
1 |
|
|
312
|
|
|
def _add_if_different( |
313
|
|
|
property_key: str, |
314
|
|
|
old_value: Any | None, |
315
|
|
|
new_value: Any | None, |
316
|
|
|
) -> None: |
317
|
|
|
if old_value != new_value: |
318
|
|
|
fields[property_key] = { |
319
|
|
|
'old': _to_str_if_not_none(old_value), |
320
|
|
|
'new': _to_str_if_not_none(new_value), |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
_add_if_different('first_name', old_first_name, new_first_name) |
324
|
1 |
|
_add_if_different('last_name', old_last_name, new_last_name) |
325
|
|
|
_add_if_different('date_of_birth', old_date_of_birth, new_date_of_birth) |
326
|
|
|
_add_if_different('country', old_country, new_country) |
327
|
|
|
_add_if_different('postal_code', old_postal_code, new_postal_code) |
328
|
|
|
_add_if_different('city', old_city, new_city) |
329
|
|
|
_add_if_different('street', old_street, new_street) |
330
|
|
|
_add_if_different('phone_number', old_phone_number, new_phone_number) |
331
|
|
|
|
332
|
1 |
|
if not fields: |
333
|
1 |
|
return Err(NothingChangedError()) |
334
|
|
|
|
335
|
1 |
|
data = { |
336
|
|
|
'fields': fields, |
337
|
|
|
'initiator_id': str(initiator.id), |
338
|
|
|
} |
339
|
1 |
|
|
340
|
|
|
entry = UserLogEntry( |
341
|
|
|
id=generate_uuid7(), |
342
|
|
|
occurred_at=occurred_at, |
343
|
1 |
|
event_type='user-details-updated', |
344
|
|
|
user_id=user.id, |
345
|
|
|
initiator_id=initiator.id, |
346
|
1 |
|
data=data, |
347
|
|
|
) |
348
|
|
|
|
349
|
|
|
return Ok(entry) |
350
|
|
|
|
351
|
|
|
|
352
|
|
|
def _to_str_if_not_none(value: Any) -> str | None: |
353
|
|
|
return str(value) if (value is not None) else None |
354
|
|
|
|