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.
Files changed (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +531 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /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, request):
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, request):
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
- """Check if user has a specific permission in JSON field."""
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)
@@ -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 set_permissions(self, value, request):
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 request.user.has_permission(USER_PERMS_PROTECTION[key]):
171
+ if not self.active_user.has_permission(USER_PERMS_PROTECTION[key]):
134
172
  raise merrors.PermissionDeniedException()
135
- elif not request.user.has_permission("manage_users"):
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[pk] = value
200
+ if self.permissions.get(pk) != value:
201
+ self.permissions[pk] = value
202
+ changed = True
162
203
  else:
163
- self.permissions[perm_key] = value
164
- self.save()
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
- self.save()
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 save(self, *args, **kwargs):
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
- self.username = self.email.split("@")[0]
188
- if not self.display_name:
189
- self.display_name = self.username
190
- super().save(*args, **kwargs)
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():
@@ -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")
@@ -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 datetime
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 = datetime.datetime.utcnow()
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 *
@@ -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)
@@ -103,8 +103,8 @@ const data = await response.json();
103
103
  "files": [
104
104
  {
105
105
  "id": 123,
106
- "filename": "document_20231201_abc12345.pdf",
107
- "original_filename": "document.pdf",
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.