django-restit 4.2.174__py3-none-any.whl → 4.2.176__py3-none-any.whl
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.
- account/migrations/0023_authsession_method.py +18 -0
- account/models/member.py +42 -29
- account/models/session.py +3 -2
- account/rpc/auth.py +23 -12
- account/rpc/oauth.py +16 -15
- account/rpc/passkeys.py +1 -1
- {django_restit-4.2.174.dist-info → django_restit-4.2.176.dist-info}/METADATA +1 -1
- {django_restit-4.2.174.dist-info → django_restit-4.2.176.dist-info}/RECORD +13 -12
- rest/__init__.py +1 -1
- rest/mail.py +7 -4
- rest/serializers/response.py +1 -1
- {django_restit-4.2.174.dist-info → django_restit-4.2.176.dist-info}/LICENSE.md +0 -0
- {django_restit-4.2.174.dist-info → django_restit-4.2.176.dist-info}/WHEEL +0 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# Generated by Django 4.2.18 on 2025-03-23 19:54
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('account', '0022_alter_memberdevice_modified'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
migrations.AddField(
|
14
|
+
model_name='authsession',
|
15
|
+
name='method',
|
16
|
+
field=models.CharField(blank=True, db_index=True, default=None, max_length=127, null=True),
|
17
|
+
),
|
18
|
+
]
|
account/models/member.py
CHANGED
@@ -149,7 +149,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
149
149
|
display_name = models.CharField(max_length=64, blank=True, null=True, default=None)
|
150
150
|
picture = models.ForeignKey("medialib.MediaItem", blank=True, null=True, help_text="Profile picture", related_name='+', on_delete=models.CASCADE)
|
151
151
|
|
152
|
-
# we use this token to allow us to invalidate JWT tokens
|
152
|
+
# we use this token to allow us to invalidate JWT tokens
|
153
153
|
security_token = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
|
154
154
|
auth_code = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
|
155
155
|
auth_code_expires = models.DateTimeField(blank=True, null=True, default=None)
|
@@ -263,6 +263,24 @@ class Member(User, RestModel, MetaDataModel):
|
|
263
263
|
return self.has_usable_password()
|
264
264
|
return False
|
265
265
|
|
266
|
+
def getFailedLoginCount(self):
|
267
|
+
c = RemoteEvents.hget("users:failed:username", self.username)
|
268
|
+
if c is not None:
|
269
|
+
return int(c)
|
270
|
+
return c
|
271
|
+
|
272
|
+
@classmethod
|
273
|
+
def GetFailedLoginsForIP(cls, ip):
|
274
|
+
c = RemoteEvents.hget("users:failed:ip", ip)
|
275
|
+
if c is not None:
|
276
|
+
return int(c)
|
277
|
+
return c
|
278
|
+
|
279
|
+
@classmethod
|
280
|
+
def GetIPsWithFailedLoginAttempts(cls, threshold):
|
281
|
+
failed_ips = RemoteEvents.hgetall("users:failed:ip")
|
282
|
+
return {ip: int(count) for ip, count in failed_ips.items() if int(count) > threshold}
|
283
|
+
|
266
284
|
def recordFailedLogin(self, request):
|
267
285
|
c = RemoteEvents.hincrby("users:failed:username", self.username, 1)
|
268
286
|
if c >= settings.LOCK_PASSWORD_ATTEMPTS:
|
@@ -273,13 +291,15 @@ class Member(User, RestModel, MetaDataModel):
|
|
273
291
|
"account", f"incorrect password for {self.username}", level=8,
|
274
292
|
error_code=498,
|
275
293
|
request=request)
|
276
|
-
|
294
|
+
ic = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
|
295
|
+
return c
|
277
296
|
|
278
297
|
def recordSuccessLogin(self, request):
|
279
298
|
self.last_login = datetime.now()
|
280
299
|
self.save()
|
281
300
|
RemoteEvents.hdel("users:failed:username", self.username)
|
282
|
-
|
301
|
+
if request:
|
302
|
+
RemoteEvents.hdel("users:failed:ip", request.ip)
|
283
303
|
|
284
304
|
def hasPasswordExpired(self):
|
285
305
|
now = datetime.now()
|
@@ -288,38 +308,33 @@ class Member(User, RestModel, MetaDataModel):
|
|
288
308
|
self.save()
|
289
309
|
return now - self.password_changed > timedelta(days=settings.PASSWORD_EXPIRES_DAYS)
|
290
310
|
|
291
|
-
def login(self, password=None, request=None
|
292
|
-
if not self.is_active or self.is_blocked:
|
293
|
-
return False
|
294
|
-
# can force login
|
311
|
+
def login(self, password=None, request=None):
|
295
312
|
if not request:
|
296
313
|
request = rh.getActiveRequest()
|
314
|
+
if not self.is_active or self.is_blocked:
|
315
|
+
return False
|
297
316
|
if not self.checkPassword(password):
|
298
|
-
# invalid password
|
299
317
|
self.recordFailedLogin(request)
|
300
318
|
return False
|
301
|
-
|
302
|
-
self.recordSuccessLogin(request)
|
303
|
-
if use_jwt:
|
304
|
-
self.recordSuccessLogin(request)
|
305
|
-
self.locateByIP(request.ip)
|
306
|
-
return True
|
307
|
-
self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
|
308
|
-
auth_login(request, self.user_ptr)
|
309
|
-
self.locateByIP(request.ip)
|
319
|
+
self.authLogin(request)
|
310
320
|
return True
|
311
321
|
|
312
322
|
def loginNoPassword(self, request=None):
|
313
323
|
if not self.is_active or self.is_blocked:
|
314
324
|
return False
|
315
|
-
# can force login
|
316
325
|
if not request:
|
317
326
|
request = rh.getActiveRequest()
|
318
|
-
self.
|
319
|
-
auth_login(request, self.user_ptr)
|
320
|
-
self.locateByIP(request.ip)
|
327
|
+
self.authLogin(request)
|
321
328
|
return True
|
322
329
|
|
330
|
+
def authLogin(self, request, use_session=True):
|
331
|
+
if use_session:
|
332
|
+
self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
|
333
|
+
auth_login(request, self.user_ptr)
|
334
|
+
if request:
|
335
|
+
self.locateByIP(request.ip)
|
336
|
+
self.recordSuccessLogin(request)
|
337
|
+
|
323
338
|
def canLogin(self, request=None, throw_exception=True, using_password=True):
|
324
339
|
if not self.is_active:
|
325
340
|
self.log("login_blocked", F"account is not active {self.username}", request, method="login", level=31)
|
@@ -577,7 +592,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
577
592
|
SMS.send(phone, msg)
|
578
593
|
return True
|
579
594
|
|
580
|
-
def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
|
595
|
+
def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
|
581
596
|
context=None):
|
582
597
|
default_domain = self.getProperty("default_domain", settings.EMAIL_DEFAULT_DOMAIN)
|
583
598
|
from_email = rh.getFromEmailForHost(default_domain)
|
@@ -675,7 +690,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
675
690
|
allow_email = not self.email_disabled and valid_email and (force or via in ["all", "email"])
|
676
691
|
else:
|
677
692
|
allow_email = valid_email
|
678
|
-
|
693
|
+
|
679
694
|
if not allow_email and not allow_sms:
|
680
695
|
return False
|
681
696
|
|
@@ -796,7 +811,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
796
811
|
def block(self, reason, request=None):
|
797
812
|
if not request:
|
798
813
|
request = self.getActiveRequest()
|
799
|
-
|
814
|
+
self.auditLog(f"account blocked, {reason}", "blocked", level=5)
|
800
815
|
RemoteEvents.hset("users:blocked:username", self.username, time.time())
|
801
816
|
self.reportIncident(
|
802
817
|
"account", f"account '{self.username}' blocked: {reason}", level=2,
|
@@ -832,7 +847,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
832
847
|
|
833
848
|
# BEGIN REST CALLBACKS
|
834
849
|
def on_permission_change(self, key, value, old_value, category):
|
835
|
-
# called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
|
850
|
+
# called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
|
836
851
|
# we want to log both the person changing permissions
|
837
852
|
# and those being changed
|
838
853
|
request = RestModel.getActiveRequest()
|
@@ -865,7 +880,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
865
880
|
request = self.getActiveRequest()
|
866
881
|
if not request.member.is_superuser:
|
867
882
|
raise PermissionDeniedException("Permission Denied: attempting to super user")
|
868
|
-
self.is_superuser = int(value)
|
883
|
+
self.is_superuser = int(value)
|
869
884
|
|
870
885
|
def set_disable(self, value):
|
871
886
|
if value is not None and value in [1, '1', True, 'true']:
|
@@ -1219,7 +1234,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
1219
1234
|
def authWS4RedisConnection(cls, auth_data):
|
1220
1235
|
"""
|
1221
1236
|
This method is used by the async/websocket service to authenticate.
|
1222
|
-
If the model can authenticate the connection it should return dict
|
1237
|
+
If the model can authenticate the connection it should return dict
|
1223
1238
|
with kind and pk of the model that is authenticaed
|
1224
1239
|
"""
|
1225
1240
|
from rest import UberDict
|
@@ -1338,5 +1353,3 @@ class AuthToken(models.Model, RestModel):
|
|
1338
1353
|
|
1339
1354
|
def __str__(self):
|
1340
1355
|
return "{o.member}: {o.secure_token}".format(o=self)
|
1341
|
-
|
1342
|
-
|
account/models/session.py
CHANGED
@@ -42,6 +42,7 @@ class AuthSession(models.Model, RestModel):
|
|
42
42
|
signature = models.CharField(max_length=127, db_index=True)
|
43
43
|
ip = models.CharField(max_length=127, null=True, blank=True, db_index=True)
|
44
44
|
buid = models.CharField(max_length=127, null=True, blank=True, default=None)
|
45
|
+
method = models.CharField(max_length=127, null=True, blank=True, db_index=True, default=None)
|
45
46
|
|
46
47
|
member = models.ForeignKey("account.Member", null=True, blank=True, related_name="auth_sessions", on_delete=models.CASCADE)
|
47
48
|
location = models.ForeignKey("location.GeoIP", related_name="auth_sessions", blank=True, null=True, default=None, on_delete=models.CASCADE)
|
@@ -72,8 +73,9 @@ class AuthSession(models.Model, RestModel):
|
|
72
73
|
return f"{self.member.username} - {self.ip} - {self.os} - {self.browser}"
|
73
74
|
|
74
75
|
@classmethod
|
75
|
-
def NewSession(cls, request):
|
76
|
+
def NewSession(cls, request, method="jwt"):
|
76
77
|
obj = cls(ip=request.ip, member=request.member, signature=request.signature)
|
78
|
+
obj.method = method
|
77
79
|
obj.user_agent = request.META.get('HTTP_USER_AGENT', None)
|
78
80
|
obj.last_activity = datetime.now()
|
79
81
|
obj.buid = request.POST.get("__buid__", request.GET.get("__buid__", None))
|
@@ -92,4 +94,3 @@ class AuthSession(models.Model, RestModel):
|
|
92
94
|
session.touch()
|
93
95
|
return session
|
94
96
|
return None
|
95
|
-
|
account/rpc/auth.py
CHANGED
@@ -37,6 +37,7 @@ def member_login(request):
|
|
37
37
|
@rd.never_cache
|
38
38
|
def jwt_login(request):
|
39
39
|
# poor mans JWT, carried over
|
40
|
+
auth_method = "basic"
|
40
41
|
username = request.DATA.get('username', None)
|
41
42
|
if not username:
|
42
43
|
return rv.restPermissionDenied(request, "Password and/or Username is incorrect", error_code=422)
|
@@ -45,29 +46,31 @@ def jwt_login(request):
|
|
45
46
|
return rv.restPermissionDenied(request, error=f"Password and/or Username is incorrect for {username}", error_code=422)
|
46
47
|
auth_code = request.DATA.get(["auth_code", "code", "invite_token"], None)
|
47
48
|
if username and auth_code:
|
49
|
+
# this is typically used for OAUTH (final)
|
48
50
|
return member_login_uname_code(request, username, auth_code)
|
49
51
|
password = request.DATA.get('password', None)
|
50
52
|
member.canLogin(request) # throws exception if cannot login
|
51
53
|
if member.requires_totp or member.has_totp:
|
54
|
+
auth_method = "basic+totp"
|
52
55
|
resp = checkForTOTP(request, member)
|
53
56
|
if resp is not None:
|
54
57
|
return resp
|
55
|
-
if not member.login(request=request, password=password
|
58
|
+
if not member.login(request=request, password=password):
|
56
59
|
# we do not want permission denied catcher invoked as it is already handled in login method
|
57
60
|
return rv.restStatus(request, False, error=f"Invalid Credentials {username}", error_code=401)
|
58
|
-
return on_complete_jwt(request, member)
|
61
|
+
return on_complete_jwt(request, member, auth_method)
|
59
62
|
|
60
63
|
|
61
|
-
def on_complete_jwt(request, member):
|
64
|
+
def on_complete_jwt(request, member, method="basic"):
|
62
65
|
if member.security_token is None or member.security_token == JWT_KEY or member.force_single_session:
|
63
66
|
member.refreshSecurityToken()
|
64
67
|
|
65
68
|
member.log(
|
66
|
-
"jwt_login", "jwt login succesful",
|
69
|
+
"jwt_login", "jwt login succesful",
|
67
70
|
request, method="login", level=7)
|
68
71
|
|
69
72
|
device_id = request.DATA.get(["device_id", "deviceID"])
|
70
|
-
|
73
|
+
|
71
74
|
token = JWToken(
|
72
75
|
user_id=member.pk,
|
73
76
|
key=member.security_token,
|
@@ -80,13 +83,13 @@ def on_complete_jwt(request, member):
|
|
80
83
|
request.signature = token.session_id
|
81
84
|
request.device_id = device_id
|
82
85
|
request.buid = request.DATA.get("__buid__", None)
|
83
|
-
request.auth_session = am.AuthSession.NewSession(request)
|
86
|
+
request.auth_session = am.AuthSession.NewSession(request, method)
|
84
87
|
if bool(device_id):
|
85
88
|
am.MemberDevice.register(request, member, device_id)
|
86
|
-
|
89
|
+
|
87
90
|
request.jwt_token = token.access_token # this tells the middleware to store in cookie
|
88
91
|
return rv.restGet(
|
89
|
-
request,
|
92
|
+
request,
|
90
93
|
dict(
|
91
94
|
access=token.access_token,
|
92
95
|
refresh=token.refresh_token,
|
@@ -218,12 +221,13 @@ def member_login_uname_code(request, username, auth_code):
|
|
218
221
|
member.setPassword(password)
|
219
222
|
member.auth_code = None
|
220
223
|
member.auth_code_expires = None
|
221
|
-
member.
|
224
|
+
member.canLogin(request, using_password=False) # throws exception if cannot login
|
225
|
+
member.loginNoPassword(request)
|
222
226
|
member.save()
|
223
227
|
member.log("code_login", "code login", request, method="login", level=8)
|
224
|
-
if request.DATA.get("auth_method") == "basic":
|
228
|
+
if request.DATA.get("auth_method") == "basic" and ALLOW_BASIC_LOGIN:
|
225
229
|
return rv.restGet(request, dict(id=member.pk, session_key=request.session.session_key))
|
226
|
-
return on_complete_jwt(request, member)
|
230
|
+
return on_complete_jwt(request, member, "auth_code")
|
227
231
|
|
228
232
|
|
229
233
|
@rd.url(r'^logout$')
|
@@ -296,7 +300,7 @@ def get_member_from_request(request):
|
|
296
300
|
request.member = member
|
297
301
|
member.log("login_blocked", "account is locked out", request, method="login", level=31)
|
298
302
|
return member, rv.restPermissionDenied(request, error=f"{member.username} Account locked out", error_code=411)
|
299
|
-
return member, None
|
303
|
+
return member, None
|
300
304
|
|
301
305
|
|
302
306
|
@rd.urlPOST('forgot')
|
@@ -410,3 +414,10 @@ def totp_verify(request):
|
|
410
414
|
return rv.restStatus(request, True)
|
411
415
|
|
412
416
|
|
417
|
+
# time based one time passwords
|
418
|
+
@rd.urlPOST('security/ip/failed_logins')
|
419
|
+
@rd.login_required
|
420
|
+
def failed_logins_by_ip(request):
|
421
|
+
ips = am.Member.GetIPsWithFailedLoginAttempts(request.DATA.get("threshold", 10))
|
422
|
+
lst = [dict(ip=k, count=v) for k, v in ips.items()]
|
423
|
+
return rv.restList(request, lst)
|
account/rpc/oauth.py
CHANGED
@@ -20,37 +20,43 @@ def oauth_google_login(request):
|
|
20
20
|
state = request.DATA.get("state")
|
21
21
|
app_url = settings.DEFAULT_LOGIN_URL
|
22
22
|
|
23
|
-
rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
|
23
|
+
# rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
|
24
24
|
|
25
25
|
if state:
|
26
|
+
# this is where we should pull out the passed in state and get the proper URL
|
26
27
|
state = objict.fromJSON(rh.hexToString(state))
|
27
28
|
rh.log_print("state", state)
|
28
29
|
app_url = state.url
|
29
30
|
|
30
31
|
if not code:
|
31
32
|
params = urlencode({'error': error})
|
32
|
-
|
33
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
34
|
+
return redirect(f"{app_url}{separator}{params}")
|
33
35
|
|
34
|
-
redirect_uri = f"{
|
36
|
+
redirect_uri = f"{request.scheme}://{request.get_host()}/{REST_PREFIX}account/oauth/google/login"
|
35
37
|
auth_data = google.getAccessToken(code, redirect_uri)
|
36
38
|
if auth_data is None or auth_data.access_token is None:
|
37
39
|
params = urlencode({'error': "failed to get access token from google"})
|
38
|
-
|
40
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
41
|
+
return redirect(f"{app_url}{separator}{params}")
|
39
42
|
|
40
43
|
user_data = google.getUserInfo(auth_data.access_token)
|
41
44
|
if user_data is None:
|
42
45
|
params = urlencode({'error': "failed to get user data from google"})
|
43
|
-
|
46
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
47
|
+
return redirect(f"{app_url}{separator}{params}")
|
44
48
|
|
45
49
|
if not user_data.email:
|
46
50
|
params = urlencode({'error': "no email with account"})
|
47
|
-
|
51
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
52
|
+
return redirect(f"{app_url}{separator}{params}")
|
48
53
|
|
49
54
|
# TODO allow new accounts?
|
50
55
|
member = Member.objects.filter(email=user_data.email).last()
|
51
56
|
if member is None:
|
52
57
|
params = urlencode({'error': "user not found"})
|
53
|
-
|
58
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
59
|
+
return redirect(f"{app_url}{separator}{params}")
|
54
60
|
|
55
61
|
member.setProperties(auth_data, category="google_auth")
|
56
62
|
member.setProperties(user_data, category="google")
|
@@ -69,13 +75,8 @@ def oauth_google_login(request):
|
|
69
75
|
member.save()
|
70
76
|
member.auditLog("user succesfully authenticated with google", "google_oauth", level=17)
|
71
77
|
|
72
|
-
params = urlencode({'oauth_code': member.auth_code, "username":member.username})
|
78
|
+
params = urlencode({'oauth_code': member.auth_code, "username":member.username, "auth_method":"google_oauth"})
|
73
79
|
rurl = None
|
74
|
-
if
|
75
|
-
|
76
|
-
rurl = f"{app_url}{params}"
|
77
|
-
else:
|
78
|
-
rurl = f"{app_url}&{params}"
|
79
|
-
else:
|
80
|
-
rurl = f"{app_url}?{params}"
|
80
|
+
separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
|
81
|
+
rurl = f"{app_url}{separator}{params}"
|
81
82
|
return redirect(rurl)
|
account/rpc/passkeys.py
CHANGED
@@ -51,4 +51,4 @@ def rest_on_passkeys_auth_complete(request):
|
|
51
51
|
request.session.pop("fido2_state"),
|
52
52
|
request.session.pop("fido2_rp_id"))
|
53
53
|
# we now want to handle the JWT or basic login flow
|
54
|
-
return on_complete_jwt(request, uk.member)
|
54
|
+
return on_complete_jwt(request, uk.member, "passkey")
|
@@ -24,17 +24,18 @@ account/migrations/0019_group_location.py,sha256=EfMB_w4qWUGDqQeNc453PFZwpjpTeoA
|
|
24
24
|
account/migrations/0020_cloudcredentials_cloudcredentialsmetadata.py,sha256=mHwxkyDfA4ueQOt34w5ndJB4XwNTDLv79CkKgzhlz-c,2250
|
25
25
|
account/migrations/0021_alter_cloudcredentials_group.py,sha256=zoFYmE-hd3uRGX6DRO9k-osPwH0jFeTU7S-pjCOtakk,561
|
26
26
|
account/migrations/0022_alter_memberdevice_modified.py,sha256=9eeKcdr9p6qFJ8ZxSnKSj1KxZjW8NZfM0YCMck6i0QQ,424
|
27
|
+
account/migrations/0023_authsession_method.py,sha256=5oIOXyj24rlrLFq7frij5VBaWk_ckh7hRPqq_jpO_b0,452
|
27
28
|
account/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
29
|
account/models/__init__.py,sha256=cV_lMnT2vL_mjiYtT4hlcIHo52ocFbGSNVkOIHHLXZY,385
|
29
30
|
account/models/device.py,sha256=8D-Sbv9PZWAnX6UVpp1lNJ03P24fknNnN1VOhqY7RVg,6306
|
30
31
|
account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
|
31
32
|
account/models/group.py,sha256=JVyMIakLskUuGXBJFyessw4LlD9Fl6AsHtpo1yZEYjk,22884
|
32
33
|
account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
|
33
|
-
account/models/member.py,sha256=
|
34
|
+
account/models/member.py,sha256=6-KbOYFyvloyiITgz5gKMyORmPtDESdzbBbMrjmz4Rs,55058
|
34
35
|
account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
|
35
36
|
account/models/notify.py,sha256=YKYEXT56i98b7-ydLt5UuEVOqW7lipQMi-KuiPhcSwY,15627
|
36
37
|
account/models/passkeys.py,sha256=lObapudvL--ABSTZTIELmYvHE3dPF0tO_KmuYk0ZJXc,1699
|
37
|
-
account/models/session.py,sha256=
|
38
|
+
account/models/session.py,sha256=5tpyRF1BHTPBL5gBIx8Eu55sWc6pTziqDpm7Zm5bdJI,3876
|
38
39
|
account/models/settings.py,sha256=gOyRWBVd3BQpjfj_hJPtqX3H46ztyRAFxBrPbv11lQg,2137
|
39
40
|
account/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
40
41
|
account/oauth/google.py,sha256=q5M6Qhpfp9QslKRVYFZBvtG6kgXV6vYMrR5fp6Xdb9I,2078
|
@@ -42,13 +43,13 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
|
|
42
43
|
account/passkeys/core.py,sha256=4aUBNCuF_kjOvE1zFapK1Pj28ap5slO71dRyfnWi0YU,4148
|
43
44
|
account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
|
44
45
|
account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
|
45
|
-
account/rpc/auth.py,sha256=
|
46
|
+
account/rpc/auth.py,sha256=MDXXhTlzTRcKB8TQ0OPawvsb0P4EeH_J3ukeSljgytk,17615
|
46
47
|
account/rpc/device.py,sha256=lU2BHNPreHV0dDTjAPc7Sc-5m2JP8SiWVqiKuBfV7Fo,2281
|
47
48
|
account/rpc/group.py,sha256=hw7iczZ6W_IrRbx5ZDw6cZ5I_ztqxhtUFJD9WR91_4s,4948
|
48
49
|
account/rpc/member.py,sha256=8XnJX-iri0Om4nc-V2_tDJzfCSzziKLw6dUx9egtEZE,2236
|
49
50
|
account/rpc/notify.py,sha256=Q2YWejP36egeF060Hih5uX4Psv_B8NWlLLPi7iDYlIw,3344
|
50
|
-
account/rpc/oauth.py,sha256=
|
51
|
-
account/rpc/passkeys.py,sha256=
|
51
|
+
account/rpc/oauth.py,sha256=1dMvEpoMcvGXEdgDq2OBZcRMnXcE1jD18-itVrZwH9A,3333
|
52
|
+
account/rpc/passkeys.py,sha256=AKgF2xgaGJi8UCFgfSMBh7rUqrJgVL8-RfDDRoPWiDM,1740
|
52
53
|
account/rpc/settings.py,sha256=EvPuwW63Gp_Va0ANIPAZ894tnS_JCctQ0FzqYRdKUNM,271
|
53
54
|
account/settings.py,sha256=XEvZdcA6p_iUpDq9NmICK8rxzIQ8NViKfrpyuYgSV4o,53
|
54
55
|
account/templates/email/base.html,sha256=GUuatccaZtO_hLLNZmMQQKew1Bjfz3e6Z7p3dM6BrWk,9669
|
@@ -379,7 +380,7 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
|
|
379
380
|
rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
|
380
381
|
rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
|
381
382
|
rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
|
382
|
-
rest/__init__.py,sha256=
|
383
|
+
rest/__init__.py,sha256=57yJpEKX4zpzd53NtME1OKpJzJqzJxz38KVjs86WJ0g,122
|
383
384
|
rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
|
384
385
|
rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
|
385
386
|
rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
|
@@ -399,7 +400,7 @@ rest/helpers.py,sha256=t7smlOUzchVno-zeq7xMJIwogAR2DeSrffWxgysOHX8,29531
|
|
399
400
|
rest/joke.py,sha256=0PpKaX2iN7jlS62kgjfmmqkFBYLPURz15aQ8R7OJkJ8,260
|
400
401
|
rest/jwtoken.py,sha256=F7Vvpm31rAplTXr8XFP-Lb4BnDB3j1B2nQq0P1iTCLQ,2576
|
401
402
|
rest/log.py,sha256=hd1_4HBOS395sfXJIL6BTw9yekm1SLgBwYx_PdfIhKA,20930
|
402
|
-
rest/mail.py,sha256=
|
403
|
+
rest/mail.py,sha256=FFvHgNcq84sJP5DyaJQKbuoao5NAsS6r-PT4MRfjWFU,8139
|
403
404
|
rest/mailman.py,sha256=v5O1G5s3HiAKmz-J1z0uT6_q3xsONPpxVl9saEyQQ2I,9174
|
404
405
|
rest/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
405
406
|
rest/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -429,7 +430,7 @@ rest/serializers/legacy.py,sha256=a5O-x2PqMKX8wYWrhCmdcivVbkPnru7UdyLbrhCaAdY,61
|
|
429
430
|
rest/serializers/localizers.py,sha256=BegaCvTQVaruhWzvGHq3zWeVFmtBChatquRqAtkke10,410
|
430
431
|
rest/serializers/model.py,sha256=08HJeqpmytjxvyiJFfsSRRG0uH-iK2mXCw6w0oMfWrI,8598
|
431
432
|
rest/serializers/profiler.py,sha256=OxOimhEyvCAuzUBC9Q1dz2xaakjAqmSnekMATsjduXM,997
|
432
|
-
rest/serializers/response.py,sha256=
|
433
|
+
rest/serializers/response.py,sha256=fsGgS9mtZSceXzNjMcjEJplisn4bG8qQSSdRGrq5Spo,8657
|
433
434
|
rest/serializers/util.py,sha256=-In89fpuVTd6_Ul8nwEUt3DjVKdpeoEyAxudlyB8K6Y,2734
|
434
435
|
rest/service.py,sha256=jl8obnMDEUzB8y3LROGPvmfKKoFU_SzOvywUQjoQZpg,4046
|
435
436
|
rest/settings_helper.py,sha256=_Vn9nmL5_GPss9zIsXzacbTQkn99NbO42CqvOZC3ge4,1532
|
@@ -516,7 +517,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
|
|
516
517
|
ws4redis/settings.py,sha256=KKq00EwoGnz1yLwCZr5Dfoq2izivmAdsNEEM4EhZwN4,1610
|
517
518
|
ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
|
518
519
|
ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
|
519
|
-
django_restit-4.2.
|
520
|
-
django_restit-4.2.
|
521
|
-
django_restit-4.2.
|
522
|
-
django_restit-4.2.
|
520
|
+
django_restit-4.2.176.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
|
521
|
+
django_restit-4.2.176.dist-info/METADATA,sha256=WVtqfF02L5jyZtQQoObLI57ZpcubGld7ZDyqc1vSXlg,7714
|
522
|
+
django_restit-4.2.176.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
523
|
+
django_restit-4.2.176.dist-info/RECORD,,
|
rest/__init__.py
CHANGED
rest/mail.py
CHANGED
@@ -37,11 +37,16 @@ SES_REGION = settings.SES_REGION
|
|
37
37
|
EMAIL_METRICS = settings.get("EMAIL_METRICS", False)
|
38
38
|
|
39
39
|
|
40
|
-
def send(to, subject, body=None, attachments=[],
|
40
|
+
def send(to, subject, body=None, attachments=[],
|
41
41
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
42
|
-
fail_silently=True, template=None,
|
42
|
+
fail_silently=True, template=None,
|
43
43
|
context=None, do_async=False, replyto=None):
|
44
44
|
# make sure to is list
|
45
|
+
if isinstance(to, str):
|
46
|
+
if "," in to:
|
47
|
+
to = [t.strip() for t in to.split(',')]
|
48
|
+
elif ";" in to:
|
49
|
+
to = [t.strip() for t in to.split(';')]
|
45
50
|
if not isinstance(to, (tuple, list)):
|
46
51
|
to = [to]
|
47
52
|
# if template lets render
|
@@ -254,5 +259,3 @@ def render_to_mail(name, context):
|
|
254
259
|
body = html_content
|
255
260
|
|
256
261
|
send(toaddrs, subject, body=body, from_email=fromaddr, do_async=True, replyto=replyto)
|
257
|
-
|
258
|
-
|
rest/serializers/response.py
CHANGED
@@ -15,7 +15,7 @@ from . import csv
|
|
15
15
|
from . import excel
|
16
16
|
# from . import profiler
|
17
17
|
STATUS_ON_PERM_DENIED = settings.get("STATUS_ON_PERM_DENIED", 403)
|
18
|
-
REST_LIST_CACHE_COUNT = settings.get("REST_LIST_CACHE_COUNT",
|
18
|
+
REST_LIST_CACHE_COUNT = settings.get("REST_LIST_CACHE_COUNT", False)
|
19
19
|
|
20
20
|
|
21
21
|
def get_query_hash(queryset):
|
File without changes
|
File without changes
|