django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__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.
- django_nativemojo-0.1.15.dist-info/METADATA +136 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +531 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/models/group.py +25 -7
- mojo/apps/account/models/member.py +15 -4
- mojo/apps/account/models/user.py +197 -20
- mojo/apps/account/rest/group.py +1 -0
- mojo/apps/account/rest/user.py +6 -2
- mojo/apps/aws/rest/__init__.py +1 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +200 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +204 -58
- mojo/apps/fileman/models/manager.py +161 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +1 -1
- mojo/apps/incident/reporter.py +3 -1
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +4 -1
- mojo/apps/metrics/utils.py +2 -2
- mojo/apps/notify/handlers/ses/message.py +1 -1
- mojo/apps/notify/providers/aws.py +2 -2
- mojo/apps/tasks/__init__.py +34 -1
- mojo/apps/tasks/manager.py +200 -45
- mojo/apps/tasks/rest/tasks.py +24 -10
- mojo/apps/tasks/runner.py +283 -18
- mojo/apps/tasks/task.py +99 -0
- mojo/apps/tasks/tq_handlers.py +118 -0
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +7 -2
- mojo/helpers/aws/__init__.py +41 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/response.py +6 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/logging.py +1 -1
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +261 -46
- mojo/models/secrets.py +13 -4
- mojo/serializers/__init__.py +100 -0
- mojo/serializers/advanced/README.md +363 -0
- mojo/serializers/advanced/__init__.py +247 -0
- mojo/serializers/advanced/formats/__init__.py +28 -0
- mojo/serializers/advanced/formats/csv.py +416 -0
- mojo/serializers/advanced/formats/excel.py +516 -0
- mojo/serializers/advanced/formats/json.py +239 -0
- mojo/serializers/advanced/formats/localizers.py +509 -0
- mojo/serializers/advanced/formats/response.py +485 -0
- mojo/serializers/advanced/serializer.py +568 -0
- mojo/serializers/manager.py +501 -0
- mojo/serializers/optimized.py +618 -0
- mojo/serializers/settings_example.py +322 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- testit/helpers.py +21 -4
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
- /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
- /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -34,7 +34,6 @@ class GroupMember(models.Model, MojoModel):
|
|
34
34
|
"default": {
|
35
35
|
"fields": [
|
36
36
|
'id',
|
37
|
-
'name',
|
38
37
|
'created',
|
39
38
|
'modified',
|
40
39
|
'is_active',
|
@@ -61,11 +60,11 @@ class GroupMember(models.Model, MojoModel):
|
|
61
60
|
return req_member.has_permission(["manage_group", "manage_members"])
|
62
61
|
return False
|
63
62
|
|
64
|
-
def set_permissions(self, value
|
63
|
+
def set_permissions(self, value):
|
65
64
|
if not isinstance(value, dict):
|
66
65
|
return
|
67
66
|
for perm, perm_value in value.items():
|
68
|
-
if not self.can_change_permission(perm, perm_value,
|
67
|
+
if not self.can_change_permission(perm, perm_value, self.active_request):
|
69
68
|
raise merrors.PermissionDeniedException()
|
70
69
|
if bool(perm_value):
|
71
70
|
self.add_permission(perm)
|
@@ -73,12 +72,24 @@ class GroupMember(models.Model, MojoModel):
|
|
73
72
|
self.remove_permission(perm)
|
74
73
|
|
75
74
|
def has_permission(self, perm_key):
|
76
|
-
"""
|
75
|
+
"""
|
76
|
+
Check if user has a specific permission—supports system-level permissions via 'sys.' prefix.
|
77
|
+
If perm_key starts with 'sys.', only the user-level permission is checked.
|
78
|
+
Otherwise, checks group-member-level permission as before.
|
79
|
+
"""
|
80
|
+
# Support lists for "OR" logic
|
77
81
|
if isinstance(perm_key, list):
|
78
82
|
for pk in perm_key:
|
79
83
|
if self.has_permission(pk):
|
80
84
|
return True
|
81
85
|
return False
|
86
|
+
|
87
|
+
# System-level: only check user permission
|
88
|
+
SYS_PREFIX = "sys."
|
89
|
+
if isinstance(perm_key, str) and perm_key.startswith(SYS_PREFIX):
|
90
|
+
bare_perm = perm_key[len(SYS_PREFIX):]
|
91
|
+
return self.user.has_permission(bare_perm)
|
92
|
+
|
82
93
|
if perm_key == "all":
|
83
94
|
return True
|
84
95
|
return self.permissions.get(perm_key, False)
|
mojo/apps/account/models/user.py
CHANGED
@@ -5,10 +5,29 @@ from mojo.helpers.settings import settings
|
|
5
5
|
from mojo import errors as merrors
|
6
6
|
from mojo.helpers import dates
|
7
7
|
from mojo.apps.account.utils.jwtoken import JWToken
|
8
|
+
from mojo.apps import metrics
|
8
9
|
import uuid
|
9
10
|
|
11
|
+
SYS_USER_PERMS_PROTECTION = {
|
12
|
+
"manage_users": "manage_users",
|
13
|
+
"manage_groups": "manage_users",
|
14
|
+
"view_logs": "manage_users",
|
15
|
+
"view_incidents": "manage_users",
|
16
|
+
"view_admin": "manage_users",
|
17
|
+
"view_taskqueue": "manage_users",
|
18
|
+
"view_global": "manage_users",
|
19
|
+
"manage_notifications": "manage_users",
|
20
|
+
"manage_files": "manage_users",
|
21
|
+
"force_single_session": "manage_users",
|
22
|
+
"file_vault": "manage_users",
|
23
|
+
"manage_aws": "manage_users"
|
24
|
+
}
|
25
|
+
|
10
26
|
USER_PERMS_PROTECTION = settings.get("USER_PERMS_PROTECTION", {})
|
27
|
+
USER_PERMS_PROTECTION.update(SYS_USER_PERMS_PROTECTION)
|
28
|
+
|
11
29
|
USER_LAST_ACTIVITY_FREQ = settings.get("USER_LAST_ACTIVITY_FREQ", 300)
|
30
|
+
METRICS_TIMEZONE = settings.get("METRICS_TIMEZONE", "America/Los_Angeles")
|
12
31
|
|
13
32
|
class CustomUserManager(BaseUserManager):
|
14
33
|
def create_user(self, email, password=None, **extra_fields):
|
@@ -62,6 +81,9 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
62
81
|
is_email_verified = models.BooleanField(default=False)
|
63
82
|
is_phone_verified = models.BooleanField(default=False)
|
64
83
|
|
84
|
+
avatar = models.ForeignKey('fileman.File', on_delete=models.SET_NULL,
|
85
|
+
null=True, blank=True, related_name='+')
|
86
|
+
|
65
87
|
USERNAME_FIELD = 'username'
|
66
88
|
objects = CustomUserManager()
|
67
89
|
|
@@ -80,12 +102,13 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
80
102
|
'id',
|
81
103
|
'display_name',
|
82
104
|
'username',
|
83
|
-
'email',
|
84
|
-
'phone_number',
|
85
105
|
'last_login',
|
86
106
|
'last_activity',
|
87
107
|
'is_active'
|
88
|
-
]
|
108
|
+
],
|
109
|
+
"graphs": {
|
110
|
+
"avatar": "basic"
|
111
|
+
}
|
89
112
|
},
|
90
113
|
"default": {
|
91
114
|
"fields": [
|
@@ -100,7 +123,15 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
100
123
|
'metadata',
|
101
124
|
'is_active'
|
102
125
|
],
|
126
|
+
"graphs": {
|
127
|
+
"avatar": "basic"
|
128
|
+
}
|
103
129
|
},
|
130
|
+
"full": {
|
131
|
+
"graphs": {
|
132
|
+
"avatar": "basic"
|
133
|
+
}
|
134
|
+
}
|
104
135
|
}
|
105
136
|
|
106
137
|
def __str__(self):
|
@@ -115,6 +146,8 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
115
146
|
|
116
147
|
def touch(self):
|
117
148
|
# can't subtract offset-naive and offset-aware datetimes
|
149
|
+
if self.last_activity and not dates.is_today(self.last_activity, METRICS_TIMEZONE):
|
150
|
+
metrics.record("user_activity_day", category="user", min_granularity="days")
|
118
151
|
if self.last_activity is None or dates.has_time_elsapsed(self.last_activity, seconds=USER_LAST_ACTIVITY_FREQ):
|
119
152
|
self.last_activity = dates.utcnow()
|
120
153
|
self.atomic_save()
|
@@ -125,19 +158,24 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
125
158
|
self.atomic_save()
|
126
159
|
return self.auth_key
|
127
160
|
|
128
|
-
def
|
161
|
+
def set_username(self, value):
|
162
|
+
if not isinstance(value, str):
|
163
|
+
raise ValueError("Username must be a string")
|
164
|
+
self.username = value
|
165
|
+
|
166
|
+
def set_permissions(self, value):
|
129
167
|
if not isinstance(value, dict):
|
130
168
|
return
|
131
169
|
for key in value:
|
132
170
|
if key in USER_PERMS_PROTECTION:
|
133
|
-
if not
|
171
|
+
if not self.active_user.has_permission(USER_PERMS_PROTECTION[key]):
|
134
172
|
raise merrors.PermissionDeniedException()
|
135
|
-
elif not
|
173
|
+
elif not self.active_user.has_permission("manage_users"):
|
136
174
|
raise merrors.PermissionDeniedException()
|
137
175
|
if bool(value[key]):
|
138
|
-
self.add_permission(key)
|
176
|
+
self.add_permission(key, commit=False)
|
139
177
|
else:
|
140
|
-
self.remove_permission(key)
|
178
|
+
self.remove_permission(key, commit=False)
|
141
179
|
|
142
180
|
def has_module_perms(self, app_label):
|
143
181
|
"""Check if user has any permissions in a given app."""
|
@@ -145,7 +183,7 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
145
183
|
|
146
184
|
def has_permission(self, perm_key):
|
147
185
|
"""Check if user has a specific permission in JSON field."""
|
148
|
-
if isinstance(perm_key, list):
|
186
|
+
if isinstance(perm_key, (list, set)):
|
149
187
|
for pk in perm_key:
|
150
188
|
if self.has_permission(pk):
|
151
189
|
return True
|
@@ -154,25 +192,39 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
154
192
|
return True
|
155
193
|
return self.permissions.get(perm_key, False)
|
156
194
|
|
157
|
-
def add_permission(self, perm_key, value=True):
|
195
|
+
def add_permission(self, perm_key, value=True, commit=True):
|
158
196
|
"""Dynamically add a permission."""
|
197
|
+
changed = False
|
159
198
|
if isinstance(perm_key, (list, set)):
|
160
199
|
for pk in perm_key:
|
161
|
-
self.permissions
|
200
|
+
if self.permissions.get(pk) != value:
|
201
|
+
self.permissions[pk] = value
|
202
|
+
changed = True
|
162
203
|
else:
|
163
|
-
self.permissions
|
164
|
-
|
204
|
+
if self.permissions.get(perm_key) != value:
|
205
|
+
self.permissions[perm_key] = value
|
206
|
+
changed = True
|
207
|
+
if changed:
|
208
|
+
self.log(f"Added permission {perm_key}", "permission:added")
|
209
|
+
if commit and changed:
|
210
|
+
self.save()
|
165
211
|
|
166
|
-
def remove_permission(self, perm_key):
|
212
|
+
def remove_permission(self, perm_key, commit=True):
|
167
213
|
"""Remove a permission."""
|
214
|
+
changed = False
|
168
215
|
if isinstance(perm_key, (list, set)):
|
169
216
|
for pk in perm_key:
|
170
217
|
if pk in self.permissions:
|
171
218
|
del self.permissions[pk]
|
219
|
+
changed = True
|
172
220
|
else:
|
173
221
|
if perm_key in self.permissions:
|
174
222
|
del self.permissions[perm_key]
|
175
|
-
|
223
|
+
changed = True
|
224
|
+
if changed:
|
225
|
+
self.log(f"Removed permission {perm_key}", "permission:removed")
|
226
|
+
if commit and changed:
|
227
|
+
self.save()
|
176
228
|
|
177
229
|
def remove_all_permissions(self):
|
178
230
|
self.permissions = {}
|
@@ -182,12 +234,137 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
182
234
|
self.set_password(value)
|
183
235
|
self.save()
|
184
236
|
|
185
|
-
def
|
237
|
+
def validate_email(self):
|
238
|
+
import re
|
239
|
+
if not self.email:
|
240
|
+
raise merrors.ValueException("Email is required")
|
241
|
+
if not re.match(r"[^@]+@[^@]+\.[^@]+", str(self.email)):
|
242
|
+
raise merrors.ValueException("Invalid email format")
|
243
|
+
return True
|
244
|
+
|
245
|
+
def validate_username(self):
|
186
246
|
if not self.username:
|
187
|
-
|
188
|
-
if
|
189
|
-
|
190
|
-
|
247
|
+
raise merrors.ValueException("Username is required")
|
248
|
+
if len(str(self.username)) <= 2:
|
249
|
+
raise merrors.ValueException("Username must be more than 2 characters")
|
250
|
+
# Check for special characters (only allow alphanumeric, underscore, dot, and @)
|
251
|
+
import re
|
252
|
+
if not re.match(r'^[a-zA-Z0-9_.@]+$', str(self.username)):
|
253
|
+
raise merrors.ValueException("Username can only contain letters, numbers, underscores, dots, and @")
|
254
|
+
# If username contains @, it must match the email field
|
255
|
+
if '@' in str(self.username) and str(self.username) != str(self.email):
|
256
|
+
raise merrors.ValueException("Username containing @ must match the email address")
|
257
|
+
return True
|
258
|
+
|
259
|
+
def set_new_password(self, new_password):
|
260
|
+
self.debug("SET NEW PASSWORD")
|
261
|
+
# Validate password strength
|
262
|
+
if len(new_password) < 8:
|
263
|
+
raise merrors.ValueException("Password must be at least 8 characters long")
|
264
|
+
|
265
|
+
strength_score = 0
|
266
|
+
|
267
|
+
# Length contributes to strength (longer is better)
|
268
|
+
if len(new_password) >= 12:
|
269
|
+
strength_score += 2
|
270
|
+
elif len(new_password) >= 10:
|
271
|
+
strength_score += 1
|
272
|
+
|
273
|
+
# Check for mixed case
|
274
|
+
has_upper = any(c.isupper() for c in new_password)
|
275
|
+
has_lower = any(c.islower() for c in new_password)
|
276
|
+
if has_upper and has_lower:
|
277
|
+
strength_score += 1
|
278
|
+
|
279
|
+
# Check for numbers
|
280
|
+
has_numbers = any(c.isdigit() for c in new_password)
|
281
|
+
if has_numbers:
|
282
|
+
strength_score += 1
|
283
|
+
|
284
|
+
# Check for special characters
|
285
|
+
import re
|
286
|
+
has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', new_password))
|
287
|
+
if has_special:
|
288
|
+
strength_score += 1
|
289
|
+
|
290
|
+
# Require minimum strength score
|
291
|
+
if strength_score < 2:
|
292
|
+
raise merrors.ValueException("Password is too weak. Use a longer password or include a mix of uppercase, lowercase, numbers, and special characters")
|
293
|
+
|
294
|
+
self.set_password(new_password)
|
295
|
+
self._set_field_change("new_password", "*", "*********")
|
296
|
+
|
297
|
+
def can_change_password(self):
|
298
|
+
if self.pk == self.active_user.pk:
|
299
|
+
return True
|
300
|
+
if self.active_user.is_superuser:
|
301
|
+
return True
|
302
|
+
if self.active_user.has_permission(["manage_users"]):
|
303
|
+
return True
|
304
|
+
return False
|
305
|
+
|
306
|
+
def generate_username_from_email(self):
|
307
|
+
"""Generate a username from email, falling back to email if username exists."""
|
308
|
+
if not self.email:
|
309
|
+
raise merrors.ValueException("Email is required to generate username")
|
310
|
+
|
311
|
+
# Try using the part before @ as username
|
312
|
+
potential_username = self.email.split("@")[0].lower()
|
313
|
+
|
314
|
+
# Check if this username already exists
|
315
|
+
qset = User.objects.filter(username=potential_username)
|
316
|
+
if self.pk is not None:
|
317
|
+
qset = qset.exclude(pk=self.pk)
|
318
|
+
|
319
|
+
# If username doesn't exist, use it
|
320
|
+
if not qset.exists():
|
321
|
+
return potential_username
|
322
|
+
|
323
|
+
# Fall back to using the full email as username
|
324
|
+
return self.email.lower()
|
325
|
+
|
326
|
+
def on_rest_pre_save(self, changed_fields, created):
|
327
|
+
self.debug("PRE SAVE")
|
328
|
+
creds_changed = False
|
329
|
+
if "email" in changed_fields:
|
330
|
+
creds_changed = True
|
331
|
+
self.validate_email()
|
332
|
+
self.email = self.email.lower()
|
333
|
+
if not self.username:
|
334
|
+
self.username = self.generate_username_from_email()
|
335
|
+
elif "@" in self.username and self.username != self.email:
|
336
|
+
self.username = self.email
|
337
|
+
qset = User.objects.filter(email=self.email)
|
338
|
+
if self.pk is not None:
|
339
|
+
qset = qset.exclude(pk=self.pk)
|
340
|
+
if qset.exists():
|
341
|
+
raise merrors.ValueException("Email already exists")
|
342
|
+
if "username" in changed_fields:
|
343
|
+
creds_changed = True
|
344
|
+
self.validate_username()
|
345
|
+
self.username = self.username.lower()
|
346
|
+
qset = User.objects.filter(username=self.username)
|
347
|
+
if self.pk is not None:
|
348
|
+
qset = qset.exclude(pk=self.pk)
|
349
|
+
if qset.exists():
|
350
|
+
raise merrors.ValueException("Username already exists")
|
351
|
+
if self.pk is not None:
|
352
|
+
# only super user can change email or username
|
353
|
+
if creds_changed and not self.active_user.is_superuser:
|
354
|
+
raise merrors.PermissionDeniedException("You are not allowed to change email or username")
|
355
|
+
if "password" in changed_fields:
|
356
|
+
raise merrors.PermissionDeniedException("You are not allowed to change password")
|
357
|
+
if "new_password" in changed_fields:
|
358
|
+
if not self.can_change_password():
|
359
|
+
raise merrors.PermissionDeniedException("You are not allowed to change password")
|
360
|
+
self.debug("CHANGING PASSWORD")
|
361
|
+
self.log("****", kind="password:changed")
|
362
|
+
if "email" in changed_fields:
|
363
|
+
self.log(kind="email:changed", log=f"{changed_fields['email']} to {self.email}")
|
364
|
+
if "username" in changed_fields:
|
365
|
+
self.log(kind="username:changed", log=f"{changed_fields['username']} to {self.username}")
|
366
|
+
self.debug("on_rest_pre_save", changed_fields, creds_changed, self.active_user.is_superuser)
|
367
|
+
|
191
368
|
|
192
369
|
def check_edit_permission(self, perms, request):
|
193
370
|
if "owner" in perms and self.is_request_user():
|
mojo/apps/account/rest/group.py
CHANGED
@@ -19,6 +19,7 @@ def on_group_me_member(request, pk=None):
|
|
19
19
|
request.group = Group.objects.filter(pk=pk).last()
|
20
20
|
if request.group is None:
|
21
21
|
return Group.rest_error_response(request, 403, error="GET permission denied: Group")
|
22
|
+
request.group.touch()
|
22
23
|
member = request.group.get_member_for_user(request.user)
|
23
24
|
if member is None:
|
24
25
|
return Group.rest_error_response(request, 403, error="GET permission denied: Member")
|
mojo/apps/account/rest/user.py
CHANGED
@@ -3,7 +3,7 @@ from mojo.apps.account.utils.jwtoken import JWToken
|
|
3
3
|
# from django.http import JsonResponse
|
4
4
|
from mojo.helpers.response import JsonResponse
|
5
5
|
from mojo.apps.account.models.user import User
|
6
|
-
import
|
6
|
+
from mojo.helpers import dates
|
7
7
|
|
8
8
|
@md.URL('user')
|
9
9
|
@md.URL('user/<int:pk>')
|
@@ -17,6 +17,8 @@ def on_user_me(request):
|
|
17
17
|
|
18
18
|
|
19
19
|
@md.POST('refresh_token')
|
20
|
+
@md.POST('token/refresh')
|
21
|
+
@md.POST("auth/token/refresh")
|
20
22
|
@md.requires_params("refresh_token")
|
21
23
|
def on_refresh_token(request):
|
22
24
|
user, error = User.validate_jwt(request.DATA.refresh_token)
|
@@ -30,6 +32,7 @@ def on_refresh_token(request):
|
|
30
32
|
|
31
33
|
|
32
34
|
@md.POST("login")
|
35
|
+
@md.POST("auth/login")
|
33
36
|
@md.requires_params("username", "password")
|
34
37
|
def on_user_login(request):
|
35
38
|
username = request.DATA.username
|
@@ -41,7 +44,8 @@ def on_user_login(request):
|
|
41
44
|
# Authentication successful
|
42
45
|
user.report_incident(f"{user.username} enter an invalid password", "invalid_password")
|
43
46
|
return JsonResponse(dict(status=False, error="Invalid username or password", code=401))
|
44
|
-
user.last_login =
|
47
|
+
user.last_login = dates.utcnow()
|
45
48
|
user.touch()
|
46
49
|
token_package = JWToken(user.get_auth_key()).create(uid=user.id)
|
50
|
+
token_package['user'] = user.to_dict("basic")
|
47
51
|
return JsonResponse(dict(status=True, data=token_package))
|
@@ -0,0 +1 @@
|
|
1
|
+
from .s3 import *
|
mojo/apps/aws/rest/s3.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo import JsonResponse
|
3
|
+
from mojo.helpers.aws import s3
|
4
|
+
|
5
|
+
|
6
|
+
@md.URL('s3/bucket')
|
7
|
+
@md.URL('s3/bucket/<str:bucket_name>')
|
8
|
+
@md.requires_perms("manage_aws")
|
9
|
+
def on_s3_bucket(request, bucket_name=None):
|
10
|
+
bucket_name = request.DATA.get('bucket_name', bucket_name)
|
11
|
+
if request.method == "GET":
|
12
|
+
if bucket_name is None:
|
13
|
+
# List all buckets
|
14
|
+
buckets = s3.S3.list_all_buckets()
|
15
|
+
return JsonResponse({
|
16
|
+
"size": len(buckets),
|
17
|
+
"count": len(buckets),
|
18
|
+
"data": buckets,
|
19
|
+
"status": True
|
20
|
+
})
|
21
|
+
else:
|
22
|
+
# Get specific bucket info
|
23
|
+
bucket = s3.S3Bucket(bucket_name)
|
24
|
+
if bucket._check_exists():
|
25
|
+
return JsonResponse({"data": {"name": bucket_name, "exists": True}, "status": True})
|
26
|
+
else:
|
27
|
+
return JsonResponse({"error": "Bucket not found", "code": 404}, status=404)
|
28
|
+
|
29
|
+
elif request.method == "POST":
|
30
|
+
if bucket_name is None:
|
31
|
+
return JsonResponse({"error": "Bucket name required"}, status=400)
|
32
|
+
|
33
|
+
# Create or update bucket
|
34
|
+
bucket = s3.S3Bucket(bucket_name)
|
35
|
+
try:
|
36
|
+
if not bucket._check_exists():
|
37
|
+
bucket.create()
|
38
|
+
bucket.enable_cors()
|
39
|
+
return JsonResponse({"message": f"Bucket {bucket_name} created successfully"})
|
40
|
+
else:
|
41
|
+
return JsonResponse({"message": f"Bucket {bucket_name} already exists"})
|
42
|
+
except Exception as e:
|
43
|
+
return JsonResponse({"error": str(e)}, status=500)
|
44
|
+
|
45
|
+
# elif request.method == "DELETE":
|
46
|
+
# if bucket_name is None:
|
47
|
+
# return JsonResponse({"error": "Bucket name required"}, status=400)
|
48
|
+
|
49
|
+
# # Check for confirmation
|
50
|
+
# if request.DATA.get("confirm_delete") != "yes delete bucket":
|
51
|
+
# return JsonResponse({"error": "Confirmation required: confirm_delete = 'yes delete bucket'"}, status=400)
|
52
|
+
|
53
|
+
# # Delete bucket
|
54
|
+
# bucket = s3.S3Bucket(bucket_name)
|
55
|
+
# try:
|
56
|
+
# if bucket._check_exists():
|
57
|
+
# bucket.delete()
|
58
|
+
# return JsonResponse({"message": f"Bucket {bucket_name} deleted successfully"})
|
59
|
+
# else:
|
60
|
+
# return JsonResponse({"error": "Bucket not found"}, status=404)
|
61
|
+
# except Exception as e:
|
62
|
+
# return JsonResponse({"error": str(e)}, status=500)
|
63
|
+
|
64
|
+
return JsonResponse({"message": "Invalid request method"}, status=405)
|
mojo/apps/fileman/README.md
CHANGED
@@ -103,8 +103,8 @@ const data = await response.json();
|
|
103
103
|
"files": [
|
104
104
|
{
|
105
105
|
"id": 123,
|
106
|
-
"
|
107
|
-
"
|
106
|
+
"storage_filename": "document_20231201_abc12345.pdf",
|
107
|
+
"filename": "document.pdf",
|
108
108
|
"upload_token": "a1b2c3d4e5f6...",
|
109
109
|
"upload_url": "https://s3.amazonaws.com/my-bucket/...",
|
110
110
|
"method": "POST",
|
@@ -297,12 +297,12 @@ class FileUploader {
|
|
297
297
|
|
298
298
|
async uploadFile(file, uploadData) {
|
299
299
|
const formData = new FormData();
|
300
|
-
|
300
|
+
|
301
301
|
// Add fields for S3 or other backends
|
302
302
|
Object.entries(uploadData.fields || {}).forEach(([key, value]) => {
|
303
303
|
formData.append(key, value);
|
304
304
|
});
|
305
|
-
|
305
|
+
|
306
306
|
formData.append('file', file);
|
307
307
|
|
308
308
|
const response = await fetch(uploadData.upload_url, {
|
@@ -411,7 +411,7 @@ file_manager = FileManager.objects.create(
|
|
411
411
|
allowed_extensions=["jpg", "jpeg", "png", "gif", "webp"],
|
412
412
|
allowed_mime_types=[
|
413
413
|
"image/jpeg",
|
414
|
-
"image/png",
|
414
|
+
"image/png",
|
415
415
|
"image/gif",
|
416
416
|
"image/webp"
|
417
417
|
],
|
@@ -429,13 +429,13 @@ def validate_upload(request, file_data):
|
|
429
429
|
# Custom business logic
|
430
430
|
if file_data['filename'].startswith('temp_'):
|
431
431
|
raise ValidationError('Temporary files not allowed')
|
432
|
-
|
432
|
+
|
433
433
|
# Check file size against user's quota
|
434
434
|
user_files_size = File.objects.filter(
|
435
435
|
uploaded_by=request.user,
|
436
436
|
upload_status=File.COMPLETED
|
437
437
|
).aggregate(total=Sum('file_size'))['total'] or 0
|
438
|
-
|
438
|
+
|
439
439
|
if user_files_size + file_data['size'] > USER_QUOTA:
|
440
440
|
raise ValidationError('Upload would exceed user quota')
|
441
441
|
```
|
@@ -546,4 +546,4 @@ print(f"Backend valid: {is_valid}, Errors: {errors}")
|
|
546
546
|
|
547
547
|
## License
|
548
548
|
|
549
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|
549
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|