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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- 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 +651 -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/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -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 +409 -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 +240 -58
- mojo/apps/fileman/models/manager.py +427 -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/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -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/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- 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.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /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)
|
mojo/apps/account/rest/group.py
CHANGED
@@ -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)
|
mojo/apps/account/rest/user.py
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
44
|
-
user.last_login =
|
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
|