django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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 (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  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 +651 -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/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,39 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps.account.models.device import UserDevice, UserDeviceLocation, GeoLocatedIP
3
+
4
+
5
+ @md.URL('user/device')
6
+ @md.URL('user/device/<int:pk>')
7
+ def on_user_device(request, pk=None):
8
+ return UserDevice.on_rest_request(request, pk)
9
+
10
+
11
+ @md.GET('user/device/lookup')
12
+ @md.requires_params('duid')
13
+ def on_user_device_by_duid(request):
14
+ duid = request.DATA.get('duid')
15
+ device = UserDevice.objects.filter(duid=duid).first()
16
+ if not device:
17
+ return UserDevice.rest_error_response(request, 404, error="Device not found")
18
+ return device.on_rest_get(request)
19
+
20
+
21
+ @md.URL('user/device/location')
22
+ @md.URL('user/device/location/<int:pk>')
23
+ def on_user_device_location(request, pk=None):
24
+ return UserDeviceLocation.on_rest_request(request, pk)
25
+
26
+
27
+ @md.URL('system/geoip')
28
+ @md.URL('system/geoip/<int:pk>')
29
+ def on_geo_located_ip(request, pk=None):
30
+ return GeoLocatedIP.on_rest_request(request, pk)
31
+
32
+
33
+ @md.GET('system/geoip/lookup')
34
+ @md.requires_params('ip')
35
+ def on_geo_located_ip_lookup(request):
36
+ ip_address = request.DATA.get('ip')
37
+ auto_refresh = request.DATA.get('auto_refresh', True)
38
+ geo_ip = GeoLocatedIP.geolocate(ip_address, auto_refresh=auto_refresh)
39
+ return geo_ip.on_rest_get(request)
@@ -1,5 +1,6 @@
1
1
  from mojo import decorators as md
2
2
  from mojo.apps.account.models import Group, GroupMember
3
+ from mojo.helpers.response import JsonResponse
3
4
 
4
5
 
5
6
  @md.URL('group')
@@ -14,11 +15,19 @@ def on_group_member(request, pk=None):
14
15
  return GroupMember.on_rest_request(request, pk)
15
16
 
16
17
 
18
+ @md.POST('group/member/invite')
19
+ @md.requires_params('email', 'group')
20
+ def on_group_invite_member(request):
21
+ ms = request.group.invite(request.DATA['email'])
22
+ return ms.on_rest_get(request)
23
+
24
+
17
25
  @md.GET('group/<int:pk>/member')
18
26
  def on_group_me_member(request, pk=None):
19
27
  request.group = Group.objects.filter(pk=pk).last()
20
28
  if request.group is None:
21
29
  return Group.rest_error_response(request, 403, error="GET permission denied: Group")
30
+ request.group.touch()
22
31
  member = request.group.get_member_for_user(request.user)
23
32
  if member is None:
24
33
  return Group.rest_error_response(request, 403, error="GET permission denied: Member")
@@ -0,0 +1,187 @@
1
+ import mojo.decorators as md
2
+ from mojo.apps.account.models import (
3
+ RegisteredDevice, NotificationTemplate, PushConfig,
4
+ NotificationDelivery
5
+ )
6
+ from mojo.apps.account.services.push import (
7
+ send_push_notification, send_direct_notification
8
+ )
9
+ from mojo.helpers import response
10
+
11
+
12
+ @md.POST('account/devices/push/register')
13
+ @md.requires_auth()
14
+ @md.requires_params(['device_token', 'device_id', 'platform'])
15
+ def register_device(request):
16
+ """
17
+ Register device for push notifications.
18
+
19
+ POST /api/account/devices/push/register
20
+ {
21
+ "device_token": "...",
22
+ "device_id": "...",
23
+ "platform": "ios|android|web",
24
+ "device_name": "...",
25
+ "app_version": "...",
26
+ "os_version": "...",
27
+ "push_preferences": {"orders": true, "marketing": false}
28
+ }
29
+ """
30
+ device, created = RegisteredDevice.objects.update_or_create(
31
+ user=request.user,
32
+ device_id=request.DATA.get('device_id'),
33
+ defaults={
34
+ 'device_token': request.DATA.get('device_token'),
35
+ 'platform': request.DATA.get('platform'),
36
+ 'device_name': request.DATA.get('device_name', ''),
37
+ 'app_version': request.DATA.get('app_version', ''),
38
+ 'os_version': request.DATA.get('os_version', ''),
39
+ 'push_preferences': request.DATA.get('push_preferences', {}),
40
+ 'is_active': True,
41
+ 'push_enabled': True
42
+ }
43
+ )
44
+
45
+ return device.on_rest_get(request, 'default')
46
+
47
+
48
+ @md.URL('account/devices/push')
49
+ @md.URL('account/devices/push/<int:pk>')
50
+ def on_registered_devices(request, pk=None):
51
+ """Standard CRUD for registered devices."""
52
+ return RegisteredDevice.on_rest_request(request, pk)
53
+
54
+
55
+ @md.URL('account/devices/push/templates')
56
+ @md.URL('account/devices/push/templates/<int:pk>')
57
+ def on_notification_templates(request, pk=None):
58
+ """Standard CRUD for notification templates."""
59
+ return NotificationTemplate.on_rest_request(request, pk)
60
+
61
+
62
+ @md.URL('account/devices/push/config')
63
+ @md.URL('account/devices/push/config/<int:pk>')
64
+ def on_push_config(request, pk=None):
65
+ """Standard CRUD for push configuration."""
66
+ return PushConfig.on_rest_request(request, pk)
67
+
68
+
69
+ @md.URL('account/devices/push/deliveries')
70
+ @md.URL('account/devices/push/deliveries/<int:pk>')
71
+ def on_notification_deliveries(request, pk=None):
72
+ """Standard CRUD for notification delivery history."""
73
+ return NotificationDelivery.on_rest_request(request, pk)
74
+
75
+
76
+ @md.POST('account/devices/push/send')
77
+ @md.requires_auth()
78
+ @md.requires_perms("send_notifications")
79
+ def send_notification(request):
80
+ """
81
+ Send push notification using template or direct content.
82
+
83
+ POST /api/account/devices/push/send
84
+
85
+ Templated:
86
+ {
87
+ "template": "template_name",
88
+ "context": {"key": "value"},
89
+ "user_ids": [1, 2, 3] # optional
90
+ }
91
+
92
+ Direct:
93
+ {
94
+ "title": "Hello!",
95
+ "body": "Your order is ready",
96
+ "category": "orders",
97
+ "action_url": "myapp://orders/123",
98
+ "user_ids": [1, 2, 3] # optional
99
+ }
100
+ """
101
+ template = request.DATA.get('template')
102
+ title = request.DATA.get('title')
103
+ body = request.DATA.get('body')
104
+
105
+ if template:
106
+ # Templated sending
107
+ context = request.DATA.get('context', {})
108
+ user_ids = request.DATA.get('user_ids')
109
+ results = send_push_notification(
110
+ user=request.user,
111
+ template_name=template,
112
+ context=context,
113
+ user_ids=user_ids
114
+ )
115
+ elif title and body:
116
+ # Direct sending
117
+ category = request.DATA.get('category', 'general')
118
+ action_url = request.DATA.get('action_url')
119
+ user_ids = request.DATA.get('user_ids')
120
+ results = send_direct_notification(
121
+ user=request.user,
122
+ title=title,
123
+ body=body,
124
+ category=category,
125
+ action_url=action_url,
126
+ user_ids=user_ids
127
+ )
128
+ else:
129
+ return response.error('Must provide either template or both title and body')
130
+
131
+ return response.success({
132
+ 'success': True,
133
+ 'sent_count': len([r for r in results if r.status == 'sent']),
134
+ 'failed_count': len([r for r in results if r.status == 'failed']),
135
+ 'deliveries': [r.to_dict("basic") for r in results]
136
+ })
137
+
138
+
139
+ @md.POST('account/devices/push/test')
140
+ @md.requires_auth()
141
+ def test_push_config(request):
142
+ """
143
+ Test push configuration by sending a test notification to requesting user's devices.
144
+
145
+ POST /api/account/devices/push/test
146
+ {
147
+ "message": "Custom test message" # optional
148
+ }
149
+ """
150
+ test_message = request.DATA.get('message', 'This is a test notification')
151
+
152
+ results = send_direct_notification(
153
+ user=request.user,
154
+ title="Push Test",
155
+ body=test_message,
156
+ category="test"
157
+ )
158
+
159
+ if not results:
160
+ return response.error('No registered devices found for testing')
161
+
162
+ return response.success({
163
+ 'success': True,
164
+ 'message': 'Test notifications sent',
165
+ 'results': [r.to_dict('basic') for r in results]
166
+ })
167
+
168
+
169
+ @md.GET('account/devices/push/stats')
170
+ @md.requires_auth()
171
+ def push_stats(request):
172
+ """
173
+ Get push notification statistics for the requesting user.
174
+ """
175
+ user_deliveries = NotificationDelivery.objects.filter(user=request.user)
176
+
177
+ stats = {
178
+ 'total_sent': user_deliveries.filter(status='sent').count(),
179
+ 'total_failed': user_deliveries.filter(status='failed').count(),
180
+ 'total_pending': user_deliveries.filter(status='pending').count(),
181
+ 'registered_devices': request.user.registered_devices.filter(is_active=True).count(),
182
+ 'enabled_devices': request.user.registered_devices.filter(
183
+ is_active=True, push_enabled=True
184
+ ).count()
185
+ }
186
+
187
+ return response.success(stats)
@@ -3,7 +3,8 @@ 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, crypto
7
+ from mojo import errors as merrors
7
8
 
8
9
  @md.URL('user')
9
10
  @md.URL('user/<int:pk>')
@@ -17,11 +18,13 @@ def on_user_me(request):
17
18
 
18
19
 
19
20
  @md.POST('refresh_token')
21
+ @md.POST('token/refresh')
22
+ @md.POST("auth/token/refresh")
20
23
  @md.requires_params("refresh_token")
21
24
  def on_refresh_token(request):
22
25
  user, error = User.validate_jwt(request.DATA.refresh_token)
23
26
  if error is not None:
24
- return JsonResponse({'error': error}, status=401)
27
+ raise merrors.PermissionDeniedException(error, 401, 401)
25
28
  # future look at keeping the refresh token the same but updating the access_token
26
29
  # TODO add device id to the token as well
27
30
  user.touch()
@@ -30,18 +33,109 @@ def on_refresh_token(request):
30
33
 
31
34
 
32
35
  @md.POST("login")
36
+ @md.POST("auth/login")
33
37
  @md.requires_params("username", "password")
34
38
  def on_user_login(request):
35
39
  username = request.DATA.username
36
40
  password = request.DATA.password
37
- user = User.objects.filter(username=username.lower().strip()).last()
41
+ from django.db.models import Q
42
+ user = User.objects.filter(Q(username=username.lower().strip()) | Q(email=username.lower().strip())).last()
38
43
  if user is None:
39
- return JsonResponse(dict(status=False, error="Invalid username or password", code=403))
44
+ User.class_report_incident(
45
+ f"login attempt with unknown username {username}",
46
+ event_type="login:unknown",
47
+ level=8,
48
+ request=request)
49
+ raise merrors.PermissionDeniedException()
40
50
  if not user.check_password(password):
41
51
  # Authentication successful
42
52
  user.report_incident(f"{user.username} enter an invalid password", "invalid_password")
43
- return JsonResponse(dict(status=False, error="Invalid username or password", code=401))
44
- user.last_login = datetime.datetime.utcnow()
53
+ raise merrors.PermissionDeniedException("Invalid username or password", 401, 401)
54
+ user.last_login = dates.utcnow()
45
55
  user.touch()
46
56
  token_package = JWToken(user.get_auth_key()).create(uid=user.id)
57
+ token_package['user'] = user.to_dict("basic")
47
58
  return JsonResponse(dict(status=True, data=token_package))
59
+
60
+
61
+ @md.POST("auth/forgot")
62
+ @md.requires_params("email")
63
+ def on_user_forgot(request):
64
+ email = request.DATA.email
65
+ user = User.objects.filter(email=email.lower().strip()).last()
66
+ if user is None:
67
+ User.class_report_incident(
68
+ f"reset password with unknown email {email}",
69
+ event_type="reset:unknown",
70
+ level=8,
71
+ request=request)
72
+ else:
73
+ user.report_incident(f"{user.username} requested a password reset", "password_reset")
74
+ if request.DATA.get("method") == "code":
75
+ code = crypto.random_string(6, True, False, False)
76
+ user.set_secret("password_reset_code", code)
77
+ user.save()
78
+ user.send_template_email("password_reset_code", dict(code=code))
79
+ elif request.DATA.get("method") in ["link", "email"]:
80
+ user.send_template_email("password_reset_link", dict(token=generate_password_reset_token(user)))
81
+ else:
82
+ raise merrors.ValueException("Invalid method")
83
+ return JsonResponse(dict(status=True, message="If email in our system a reset email was sent."))
84
+
85
+
86
+ def generate_password_reset_token(user):
87
+ token = crypto.b64_encode({"uid":user.pk, "r": crypto.random_string(6, True, True, False)})
88
+ sig = crypto.sign(token, user.get_auth_key())
89
+ hex_token = token.encode("utf-8").hex() + sig[-6:]
90
+ return hex_token
91
+
92
+
93
+ def verify_password_reset_token(hex_token):
94
+ orig_token = hex_token
95
+ try:
96
+ tsig = hex_token[-6:]
97
+ hex_token = hex_token[:-6]
98
+ token = bytes.fromhex(hex_token).decode("utf-8")
99
+ obj = crypto.b64_decode(token)
100
+ if not isinstance(obj, dict) or "uid" not in obj:
101
+ raise merrors.ValueException("Invalid token")
102
+ user = User.objects.get(pk=obj["uid"])
103
+ sig = crypto.sign(token, user.get_auth_key())
104
+ if sig[-6:] != tsig:
105
+ user.report_incident(f"{user.username} invalid reset token", "invalid_reset_token")
106
+ raise merrors.ValueException("Invalid token")
107
+ return user
108
+ except Exception:
109
+ pass
110
+ User.class_report_incident(
111
+ "invalid reset token",
112
+ event_type="reset:unknown",
113
+ level=8, token=orig_token)
114
+ raise merrors.ValueException("Invalid token")
115
+
116
+
117
+ @md.POST("auth/password/reset/code")
118
+ @md.requires_params("code", "email", "new_password")
119
+ def on_user_password_reset_code(request):
120
+ code = request.DATA.get("code")
121
+ email = request.DATA.get("email")
122
+ new_password = request.DATA.get("new_password")
123
+ user = User.objects.get(email=email)
124
+ sec_code = user.get_secret("password_reset_code")
125
+ if len(sec_code) != 6 or len(code) != 6 or code != sec_code:
126
+ user.report_incident(f"{user.username} invalid password reset code", "password_reset")
127
+ raise merrors.ValueException("Invalid code")
128
+ user.set_password(new_password)
129
+ user.save()
130
+ return JsonResponse(dict(status=True, message="Password reset successful."))
131
+
132
+
133
+ @md.POST("auth/password/reset/token")
134
+ @md.requires_params("token", "new_password")
135
+ def on_user_password_reset_token(request):
136
+ token = request.DATA.get("token")
137
+ user = verify_password_reset_token(token)
138
+ new_password = request.DATA.get("new_password")
139
+ user.set_password(new_password)
140
+ user.save()
141
+ return JsonResponse(dict(status=True, message="Password reset successful."))
@@ -0,0 +1 @@
1
+ # Push notification services