Mobile app messaging backend (#874)
* move mobile notifications to a separate backend, remove critical notification * remove outdated mobile app code * MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED -> FEATURE_MOBILE_APP_INTEGRATION_ENABLED * create error log if no devices are set up * move mobile auth related code to the mobile_app Django app * move mobile auth related code to the mobile_app Django app * move mobile auth related code to the mobile_app Django app * fix typing * add GCMDevice todos * add user connection capabilities * add user connect/disconnect to the messaging backend * move APNS endpoint to mobile_app Django app * restore critical notifications * support hackathon app * tweak migrations so mobile app auth tokens are preserved * reuse notify_by IDs * use mobile app template to render push notification * add GCM/FCM (Android) support * fix unlink user * logger.error -> logger.info
This commit is contained in:
parent
d0dbc4207a
commit
255964ceaf
30 changed files with 386 additions and 263 deletions
|
|
@ -5,10 +5,8 @@ from django.conf import settings
|
|||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from kombu import uuid as celery_uuid
|
||||
from push_notifications.models import APNSDevice
|
||||
|
||||
from apps.alerts.constants import NEXT_ESCALATION_DELAY
|
||||
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
||||
from apps.alerts.signals import user_notification_action_triggered_signal
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
|
|
@ -348,47 +346,6 @@ def perform_notification(log_record_pk):
|
|||
notification_channel=notification_channel,
|
||||
notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK,
|
||||
).save()
|
||||
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL:
|
||||
message = f"{AlertGroupWebRenderer(alert_group).render().get('title', 'Incident')}"
|
||||
thread_id = f"{alert_group.channel.organization.public_primary_key}:{alert_group.public_primary_key}"
|
||||
devices_to_notify = APNSDevice.objects.filter(user_id=user.pk)
|
||||
devices_to_notify.send_message(
|
||||
message,
|
||||
thread_id=thread_id,
|
||||
category="USER_NEW_INCIDENT",
|
||||
extra={
|
||||
"orgId": f"{alert_group.channel.organization.public_primary_key}",
|
||||
"orgName": f"{alert_group.channel.organization.stack_slug}",
|
||||
"incidentId": f"{alert_group.public_primary_key}",
|
||||
"status": f"{alert_group.status}",
|
||||
"aps": {
|
||||
"alert": f"{message}",
|
||||
"sound": "bingbong.aiff",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL:
|
||||
message = f"{AlertGroupWebRenderer(alert_group).render().get('title', 'Incident')}"
|
||||
thread_id = f"{alert_group.channel.organization.public_primary_key}:{alert_group.public_primary_key}"
|
||||
devices_to_notify = APNSDevice.objects.filter(user_id=user.pk)
|
||||
devices_to_notify.send_message(
|
||||
message,
|
||||
thread_id=thread_id,
|
||||
category="USER_NEW_INCIDENT",
|
||||
extra={
|
||||
"orgId": f"{alert_group.channel.organization.public_primary_key}",
|
||||
"orgName": f"{alert_group.channel.organization.stack_slug}",
|
||||
"incidentId": f"{alert_group.public_primary_key}",
|
||||
"status": f"{alert_group.status}",
|
||||
"aps": {
|
||||
"alert": f"Critical page: {message}",
|
||||
"interruption-level": "critical",
|
||||
"sound": "ambulance.aiff",
|
||||
},
|
||||
},
|
||||
)
|
||||
else:
|
||||
try:
|
||||
backend_id = UserNotificationPolicy.NotificationChannel(notification_policy.notify_by).name
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.conf import settings
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from apps.mobile_app.views import APNSDeviceAuthorizedViewSet
|
||||
from common.api_helpers.optional_slash_router import OptionalSlashRouter, optional_slash_path
|
||||
|
||||
from .views import UserNotificationPolicyView, auth
|
||||
|
|
@ -8,7 +9,6 @@ from .views.alert_group import AlertGroupView
|
|||
from .views.alert_receive_channel import AlertReceiveChannelView
|
||||
from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView
|
||||
from .views.alerts import AlertDetailView
|
||||
from .views.apns_device import APNSDeviceAuthorizedViewSet
|
||||
from .views.channel_filter import ChannelFilterView
|
||||
from .views.custom_button import CustomButtonView
|
||||
from .views.escalation_chain import EscalationChainViewSet
|
||||
|
|
@ -68,7 +68,8 @@ router.register(r"tokens", PublicApiTokenView, basename="api_token")
|
|||
router.register(r"live_settings", LiveSettingViewSet, basename="live_settings")
|
||||
router.register(r"oncall_shifts", OnCallShiftView, basename="oncall_shifts")
|
||||
|
||||
if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
|
||||
# TODO: remove this when the hackathon app is deprecated (APNSDeviceAuthorizedViewSet is registered in mobile_app)
|
||||
if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
|
||||
router.register(r"device/apns", APNSDeviceAuthorizedViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ from apps.alerts.constants import ActionSource
|
|||
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
|
||||
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
|
||||
from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer
|
||||
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from apps.user_management.models import User
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet
|
||||
|
||||
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
||||
|
||||
|
||||
class APNSDeviceAuthorizedViewSet(APNSDeviceAuthorizedViewSet):
|
||||
authentication_classes = (MobileAppAuthTokenAuthentication, PluginAuthentication)
|
||||
|
|
@ -36,7 +36,7 @@ class FeaturesAPIView(APIView):
|
|||
if settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED:
|
||||
enabled_features.append(FEATURE_TELEGRAM)
|
||||
|
||||
if settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
|
||||
if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
|
||||
mobile_app_settings = DynamicSetting.objects.get_or_create(
|
||||
name="mobile_app_settings",
|
||||
defaults={
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ from rest_framework import mixins, viewsets
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from apps.api.serializers.team import TeamSerializer
|
||||
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
from apps.user_management.models import Team
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,17 +25,13 @@ from apps.api.permissions import (
|
|||
)
|
||||
from apps.api.serializers.team import TeamSerializer
|
||||
from apps.api.serializers.user import FilterUserSerializer, UserHiddenFieldsSerializer, UserSerializer
|
||||
from apps.auth_token.auth import (
|
||||
MobileAppAuthTokenAuthentication,
|
||||
MobileAppVerificationTokenAuthentication,
|
||||
PluginAuthentication,
|
||||
)
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
|
||||
from apps.auth_token.models import UserScheduleExportAuthToken
|
||||
from apps.auth_token.models.mobile_app_auth_token import MobileAppAuthToken
|
||||
from apps.auth_token.models.mobile_app_verification_token import MobileAppVerificationToken
|
||||
from apps.base.messaging import get_messaging_backend_from_id
|
||||
from apps.base.utils import live_settings
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication, MobileAppVerificationTokenAuthentication
|
||||
from apps.mobile_app.models import MobileAppAuthToken
|
||||
from apps.telegram.client import TelegramClient
|
||||
from apps.telegram.models import TelegramVerificationCode
|
||||
from apps.twilioapp.phone_manager import PhoneManager
|
||||
|
|
@ -473,62 +469,6 @@ class UserView(
|
|||
raise NotFound
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=True, methods=["get", "post", "delete"])
|
||||
def mobile_app_verification_token(self, request, pk):
|
||||
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
||||
|
||||
if not settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
mobile_app_settings = DynamicSetting.objects.get_or_create(
|
||||
name="mobile_app_settings",
|
||||
defaults={
|
||||
"json_value": {
|
||||
"org_ids": [],
|
||||
}
|
||||
},
|
||||
)[0]
|
||||
if self.request.auth.organization.pk not in mobile_app_settings.json_value["org_ids"]:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
user = self.get_object()
|
||||
|
||||
if self.request.method == "GET":
|
||||
try:
|
||||
token = MobileAppVerificationToken.objects.get(user=user)
|
||||
except MobileAppVerificationToken.DoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
response = {
|
||||
"token_id": token.id,
|
||||
"user_id": token.user_id,
|
||||
"organization_id": token.organization_id,
|
||||
"created_at": token.created_at,
|
||||
"revoked_at": token.revoked_at,
|
||||
}
|
||||
return Response(response, status=status.HTTP_200_OK)
|
||||
|
||||
if self.request.method == "POST":
|
||||
# If token already exists revoke it
|
||||
try:
|
||||
token = MobileAppVerificationToken.objects.get(user=user)
|
||||
token.delete()
|
||||
except MobileAppVerificationToken.DoesNotExist:
|
||||
pass
|
||||
|
||||
instance, token = MobileAppVerificationToken.create_auth_token(user, user.organization)
|
||||
data = {"id": instance.pk, "token": token, "created_at": instance.created_at}
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
||||
if self.request.method == "DELETE":
|
||||
try:
|
||||
token = MobileAppVerificationToken.objects.get(user=user)
|
||||
token.delete()
|
||||
except MobileAppVerificationToken.DoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(
|
||||
methods=["get", "post", "delete"],
|
||||
detail=False,
|
||||
|
|
@ -537,7 +477,7 @@ class UserView(
|
|||
def mobile_app_auth_token(self, request):
|
||||
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
||||
|
||||
if not settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
|
||||
if not settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
mobile_app_settings = DynamicSetting.objects.get_or_create(
|
||||
|
|
@ -582,7 +522,7 @@ class UserView(
|
|||
try:
|
||||
token = MobileAppAuthToken.objects.get(user=self.request.user)
|
||||
token.delete()
|
||||
except MobileAppVerificationToken.DoesNotExist:
|
||||
except MobileAppAuthToken.DoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from rest_framework import status
|
||||
|
|
@ -150,7 +149,6 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet):
|
|||
"""
|
||||
Returns list of options for user notification policies dropping options that requires disabled features.
|
||||
"""
|
||||
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
||||
choices = []
|
||||
for notification_channel in NotificationChannelAPIOptions.AVAILABLE_FOR_USE:
|
||||
slack_integration_required = (
|
||||
|
|
@ -160,16 +158,10 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet):
|
|||
notification_channel
|
||||
in NotificationChannelAPIOptions.TELEGRAM_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS
|
||||
)
|
||||
mobile_app_integration_required = (
|
||||
notification_channel
|
||||
in NotificationChannelAPIOptions.MOBILE_APP_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS
|
||||
)
|
||||
if slack_integration_required and not settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
||||
continue
|
||||
if telegram_integration_required and not settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED:
|
||||
continue
|
||||
if mobile_app_integration_required and not settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED:
|
||||
continue
|
||||
|
||||
# extra backends may be enabled per organization
|
||||
built_in_backend_names = {b[0] for b in BUILT_IN_BACKENDS}
|
||||
|
|
@ -178,20 +170,6 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet):
|
|||
if extra_messaging_backend is None:
|
||||
continue
|
||||
|
||||
mobile_app_settings = DynamicSetting.objects.get_or_create(
|
||||
name="mobile_app_settings",
|
||||
defaults={
|
||||
"json_value": {
|
||||
"org_ids": [],
|
||||
}
|
||||
},
|
||||
)[0]
|
||||
if (
|
||||
mobile_app_integration_required
|
||||
and settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED
|
||||
and self.request.auth.organization.pk not in mobile_app_settings.json_value["org_ids"]
|
||||
):
|
||||
continue
|
||||
choices.append(
|
||||
{
|
||||
"value": notification_channel,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ from common.constants.role import Role
|
|||
from .constants import SCHEDULE_EXPORT_TOKEN_NAME, SLACK_AUTH_TOKEN_NAME
|
||||
from .exceptions import InvalidToken
|
||||
from .models import ApiAuthToken, PluginAuthToken, ScheduleExportAuthToken, SlackAuthToken, UserScheduleExportAuthToken
|
||||
from .models.mobile_app_auth_token import MobileAppAuthToken
|
||||
from .models.mobile_app_verification_token import MobileAppVerificationToken
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -215,39 +213,3 @@ class UserScheduleExportAuthentication(BaseAuthentication):
|
|||
raise exceptions.AuthenticationFailed("Export token is deactivated")
|
||||
|
||||
return auth_token.user, auth_token
|
||||
|
||||
|
||||
class MobileAppVerificationTokenAuthentication(BaseAuthentication):
|
||||
model = MobileAppVerificationToken
|
||||
|
||||
def authenticate(self, request) -> Tuple[User, MobileAppVerificationToken]:
|
||||
auth = get_authorization_header(request).decode("utf-8")
|
||||
user, auth_token = self.authenticate_credentials(auth)
|
||||
return user, auth_token
|
||||
|
||||
def authenticate_credentials(self, token_string: str) -> Tuple[User, MobileAppVerificationToken]:
|
||||
try:
|
||||
auth_token = self.model.validate_token_string(token_string)
|
||||
except InvalidToken:
|
||||
raise exceptions.AuthenticationFailed("Invalid token")
|
||||
|
||||
return auth_token.user, auth_token
|
||||
|
||||
|
||||
class MobileAppAuthTokenAuthentication(BaseAuthentication):
|
||||
model = MobileAppAuthToken
|
||||
|
||||
def authenticate(self, request) -> Tuple[User, MobileAppAuthToken]:
|
||||
auth = get_authorization_header(request).decode("utf-8")
|
||||
user, auth_token = self.authenticate_credentials(auth)
|
||||
if user is None:
|
||||
return None
|
||||
return user, auth_token
|
||||
|
||||
def authenticate_credentials(self, token_string: str) -> Tuple[User, MobileAppAuthToken]:
|
||||
try:
|
||||
auth_token = self.model.validate_token_string(token_string)
|
||||
except InvalidToken:
|
||||
return None, None
|
||||
|
||||
return auth_token.user, auth_token
|
||||
|
|
|
|||
|
|
@ -8,5 +8,3 @@ SLACK_AUTH_TOKEN_NAME = "slack_login_token"
|
|||
|
||||
SCHEDULE_EXPORT_TOKEN_NAME = "token"
|
||||
SCHEDULE_EXPORT_TOKEN_CHARACTER_LENGTH = 32
|
||||
|
||||
MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS = 60
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Generated by Django 3.2.5 on 2022-05-31 14:46
|
||||
from django.utils import timezone
|
||||
|
||||
import apps.auth_token.models.mobile_app_verification_token
|
||||
import apps.auth_token.models.slack_auth_token
|
||||
from django.db import migrations, models
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
|
|||
('digest', models.CharField(max_length=128)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(null=True)),
|
||||
('expire_date', models.DateTimeField(default=apps.auth_token.models.mobile_app_verification_token.get_expire_date)),
|
||||
('expire_date', models.DateTimeField(default=timezone.now)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
|
|
|||
37
engine/apps/auth_token/migrations/0003_auto_20221121_1610.py
Normal file
37
engine/apps/auth_token/migrations/0003_auto_20221121_1610.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.2.16 on 2022-11-21 16:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth_token', '0002_squashed_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='mobileappverificationtoken',
|
||||
name='organization',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='mobileappverificationtoken',
|
||||
name='user',
|
||||
),
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.DeleteModel(
|
||||
name='MobileAppAuthToken',
|
||||
),
|
||||
],
|
||||
database_operations=[
|
||||
migrations.AlterModelTable(
|
||||
name='MobileAppAuthToken',
|
||||
table='mobile_app_mobileappauthtoken',
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='MobileAppVerificationToken',
|
||||
),
|
||||
]
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
from typing import Tuple
|
||||
|
||||
from django.db import models
|
||||
|
||||
from apps.auth_token import constants, crypto
|
||||
from apps.auth_token.models.base_auth_token import BaseAuthToken
|
||||
from apps.user_management.models import Organization, User
|
||||
|
||||
|
||||
class MobileAppAuthToken(BaseAuthToken):
|
||||
user = models.ForeignKey(
|
||||
to=User, null=False, blank=False, related_name="mobile_app_auth_tokens", on_delete=models.CASCADE
|
||||
)
|
||||
organization = models.ForeignKey(
|
||||
to=Organization, null=False, blank=False, related_name="mobile_app_auth_tokens", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_auth_token(cls, user: User, organization: Organization) -> Tuple["MobileAppAuthToken", str]:
|
||||
token_string = crypto.generate_token_string()
|
||||
digest = crypto.hash_token_string(token_string)
|
||||
|
||||
instance = cls.objects.create(
|
||||
token_key=token_string[: constants.TOKEN_KEY_LENGTH],
|
||||
digest=digest,
|
||||
user=user,
|
||||
organization=organization,
|
||||
)
|
||||
return instance, token_string
|
||||
|
|
@ -35,8 +35,6 @@ BUILT_IN_BACKENDS = (
|
|||
("SMS", 1),
|
||||
("PHONE_CALL", 2),
|
||||
("TELEGRAM", 3),
|
||||
("MOBILE_PUSH_GENERAL", 5),
|
||||
("MOBILE_PUSH_CRITICAL", 6),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -201,8 +199,6 @@ class NotificationChannelOptions:
|
|||
UserNotificationPolicy.NotificationChannel.SMS,
|
||||
UserNotificationPolicy.NotificationChannel.PHONE_CALL,
|
||||
UserNotificationPolicy.NotificationChannel.TELEGRAM,
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL,
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL,
|
||||
] + [
|
||||
getattr(UserNotificationPolicy.NotificationChannel, backend_id)
|
||||
for backend_id, b in get_messaging_backends()
|
||||
|
|
@ -213,10 +209,6 @@ class NotificationChannelOptions:
|
|||
|
||||
SLACK_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [UserNotificationPolicy.NotificationChannel.SLACK]
|
||||
TELEGRAM_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [UserNotificationPolicy.NotificationChannel.TELEGRAM]
|
||||
MOBILE_APP_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL,
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL,
|
||||
]
|
||||
|
||||
|
||||
class NotificationChannelAPIOptions(NotificationChannelOptions):
|
||||
|
|
@ -225,8 +217,6 @@ class NotificationChannelAPIOptions(NotificationChannelOptions):
|
|||
UserNotificationPolicy.NotificationChannel.SMS: "SMS \U00002709\U0001F4F2",
|
||||
UserNotificationPolicy.NotificationChannel.PHONE_CALL: "Phone call \U0000260E",
|
||||
UserNotificationPolicy.NotificationChannel.TELEGRAM: "Telegram \U0001F916",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "Mobile App",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "Mobile App Critical",
|
||||
}
|
||||
LABELS.update(
|
||||
{
|
||||
|
|
@ -240,8 +230,6 @@ class NotificationChannelAPIOptions(NotificationChannelOptions):
|
|||
UserNotificationPolicy.NotificationChannel.SMS: "SMS",
|
||||
UserNotificationPolicy.NotificationChannel.PHONE_CALL: "\U0000260E",
|
||||
UserNotificationPolicy.NotificationChannel.TELEGRAM: "Telegram",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "Mobile App",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "Mobile App Critical",
|
||||
}
|
||||
SHORT_LABELS.update(
|
||||
{
|
||||
|
|
@ -257,8 +245,6 @@ class NotificationChannelPublicAPIOptions(NotificationChannelAPIOptions):
|
|||
UserNotificationPolicy.NotificationChannel.SMS: "notify_by_sms",
|
||||
UserNotificationPolicy.NotificationChannel.PHONE_CALL: "notify_by_phone_call",
|
||||
UserNotificationPolicy.NotificationChannel.TELEGRAM: "notify_by_telegram",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "notify_by_mobile_app",
|
||||
UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "notify_by_mobile_app_critical",
|
||||
}
|
||||
LABELS.update(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -287,10 +287,6 @@ class UserNotificationPolicyLogRecord(models.Model):
|
|||
result += f"called {user_verbal} by phone"
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.TELEGRAM:
|
||||
result += f"sent telegram message to {user_verbal}"
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL:
|
||||
result += f"sent push notifications to {user_verbal}"
|
||||
elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL:
|
||||
result += f"sent push critical notifications to {user_verbal}"
|
||||
elif notification_channel is None:
|
||||
result += f"invited {user_verbal} but notification channel is unspecified"
|
||||
else:
|
||||
|
|
|
|||
0
engine/apps/mobile_app/__init__.py
Normal file
0
engine/apps/mobile_app/__init__.py
Normal file
13
engine/apps/mobile_app/alert_rendering.py
Normal file
13
engine/apps/mobile_app/alert_rendering.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater
|
||||
from common.utils import str_or_backup
|
||||
|
||||
|
||||
class AlertMobileAppTemplater(AlertTemplater):
|
||||
def _render_for(self):
|
||||
return "MOBILE_APP"
|
||||
|
||||
|
||||
def get_push_notification_message(alert_group):
|
||||
alert = alert_group.alerts.first()
|
||||
templated_alert = AlertMobileAppTemplater(alert).render()
|
||||
return str_or_backup(templated_alert.title, "Alert Group")
|
||||
45
engine/apps/mobile_app/auth.py
Normal file
45
engine/apps/mobile_app/auth.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from typing import Optional, Tuple
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
|
||||
from apps.auth_token.exceptions import InvalidToken
|
||||
from apps.user_management.models import User
|
||||
|
||||
from .models import MobileAppAuthToken, MobileAppVerificationToken
|
||||
|
||||
|
||||
class MobileAppVerificationTokenAuthentication(BaseAuthentication):
|
||||
model = MobileAppVerificationToken
|
||||
|
||||
def authenticate(self, request) -> Tuple[User, MobileAppVerificationToken]:
|
||||
auth = get_authorization_header(request).decode("utf-8")
|
||||
user, auth_token = self.authenticate_credentials(auth)
|
||||
return user, auth_token
|
||||
|
||||
def authenticate_credentials(self, token_string: str) -> Tuple[User, MobileAppVerificationToken]:
|
||||
try:
|
||||
auth_token = self.model.validate_token_string(token_string)
|
||||
except InvalidToken:
|
||||
raise exceptions.AuthenticationFailed("Invalid token")
|
||||
|
||||
return auth_token.user, auth_token
|
||||
|
||||
|
||||
class MobileAppAuthTokenAuthentication(BaseAuthentication):
|
||||
model = MobileAppAuthToken
|
||||
|
||||
def authenticate(self, request) -> Optional[Tuple[User, MobileAppAuthToken]]:
|
||||
auth = get_authorization_header(request).decode("utf-8")
|
||||
user, auth_token = self.authenticate_credentials(auth)
|
||||
if user is None:
|
||||
return None
|
||||
return user, auth_token
|
||||
|
||||
def authenticate_credentials(self, token_string: str) -> Tuple[Optional[User], Optional[MobileAppAuthToken]]:
|
||||
try:
|
||||
auth_token = self.model.validate_token_string(token_string)
|
||||
except InvalidToken:
|
||||
return None, None
|
||||
|
||||
return auth_token.user, auth_token
|
||||
56
engine/apps/mobile_app/backend.py
Normal file
56
engine/apps/mobile_app/backend.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from push_notifications.models import APNSDevice
|
||||
|
||||
from apps.base.messaging import BaseMessagingBackend
|
||||
from apps.mobile_app.tasks import notify_user_async
|
||||
|
||||
|
||||
class MobileAppBackend(BaseMessagingBackend):
|
||||
backend_id = "MOBILE_APP"
|
||||
label = "Mobile app"
|
||||
short_label = "Mobile app"
|
||||
available_for_use = True
|
||||
template_fields = ["title"]
|
||||
|
||||
# TODO: add QR code generation (base64 encode?)
|
||||
def generate_user_verification_code(self, user):
|
||||
from apps.mobile_app.models import MobileAppVerificationToken
|
||||
|
||||
# remove existing token before creating a new one
|
||||
MobileAppVerificationToken.objects.filter(user=user).delete()
|
||||
|
||||
_, token = MobileAppVerificationToken.create_auth_token(user, user.organization)
|
||||
return token
|
||||
|
||||
def unlink_user(self, user):
|
||||
from apps.mobile_app.models import MobileAppAuthToken
|
||||
|
||||
token = MobileAppAuthToken.objects.get(user=user)
|
||||
token.delete()
|
||||
|
||||
def serialize_user(self, user):
|
||||
# TODO: add Android support using GCMDevice
|
||||
return {"connected": APNSDevice.objects.filter(user_id=user.pk).exists()}
|
||||
|
||||
def notify_user(self, user, alert_group, notification_policy, critical=False):
|
||||
notify_user_async.delay(
|
||||
user_pk=user.pk,
|
||||
alert_group_pk=alert_group.pk,
|
||||
notification_policy_pk=notification_policy.pk,
|
||||
critical=critical,
|
||||
)
|
||||
|
||||
|
||||
class MobileAppCriticalBackend(MobileAppBackend):
|
||||
"""
|
||||
This notification backend should not exist, criticality of the push notification should be an option passed to the
|
||||
MobileAppBackend messaging backend.
|
||||
TODO: add ability to pass options to messaging backends both on backend and frontend, delete this backend after that
|
||||
"""
|
||||
|
||||
backend_id = "MOBILE_APP_CRITICAL"
|
||||
label = "Mobile app critical"
|
||||
short_label = "Mobile app critical"
|
||||
template_fields = []
|
||||
|
||||
def notify_user(self, user, alert_group, notification_policy, critical=True):
|
||||
super().notify_user(user, alert_group, notification_policy, critical)
|
||||
54
engine/apps/mobile_app/migrations/0001_initial.py
Normal file
54
engine/apps/mobile_app/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Generated by Django 3.2.16 on 2022-11-21 16:10
|
||||
|
||||
import apps.mobile_app.models
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('user_management', '0004_auto_20221025_0316'),
|
||||
('auth_token', '0003_auto_20221121_1610'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MobileAppVerificationToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token_key', models.CharField(db_index=True, max_length=8)),
|
||||
('digest', models.CharField(max_length=128)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(null=True)),
|
||||
('expire_date', models.DateTimeField(default=apps.mobile_app.models.get_expire_date)),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mobile_app_verification_token_set', to='user_management.organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mobile_app_verification_token_set', to='user_management.user')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.SeparateDatabaseAndState(
|
||||
state_operations=[
|
||||
migrations.CreateModel(
|
||||
name='MobileAppAuthToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token_key', models.CharField(db_index=True, max_length=8)),
|
||||
('digest', models.CharField(max_length=128)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(null=True)),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mobile_app_auth_tokens', to='user_management.organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mobile_app_auth_tokens', to='user_management.user')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
],
|
||||
database_operations=[],
|
||||
)
|
||||
]
|
||||
0
engine/apps/mobile_app/migrations/__init__.py
Normal file
0
engine/apps/mobile_app/migrations/__init__.py
Normal file
|
|
@ -4,10 +4,11 @@ from django.db import models
|
|||
from django.utils import timezone
|
||||
|
||||
from apps.auth_token import constants, crypto
|
||||
from apps.auth_token.constants import MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS
|
||||
from apps.auth_token.models import BaseAuthToken
|
||||
from apps.user_management.models import Organization, User
|
||||
|
||||
MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
def get_expire_date():
|
||||
return timezone.now() + timezone.timedelta(seconds=MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS)
|
||||
|
|
@ -46,3 +47,25 @@ class MobileAppVerificationToken(BaseAuthToken):
|
|||
organization=organization,
|
||||
)
|
||||
return instance, token_string
|
||||
|
||||
|
||||
class MobileAppAuthToken(BaseAuthToken):
|
||||
user = models.ForeignKey(
|
||||
to=User, null=False, blank=False, related_name="mobile_app_auth_tokens", on_delete=models.CASCADE
|
||||
)
|
||||
organization = models.ForeignKey(
|
||||
to=Organization, null=False, blank=False, related_name="mobile_app_auth_tokens", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_auth_token(cls, user: User, organization: Organization) -> Tuple["MobileAppAuthToken", str]:
|
||||
token_string = crypto.generate_token_string()
|
||||
digest = crypto.hash_token_string(token_string)
|
||||
|
||||
instance = cls.objects.create(
|
||||
token_key=token_string[: constants.TOKEN_KEY_LENGTH],
|
||||
digest=digest,
|
||||
user=user,
|
||||
organization=organization,
|
||||
)
|
||||
return instance, token_string
|
||||
95
engine/apps/mobile_app/tasks.py
Normal file
95
engine/apps/mobile_app/tasks.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from push_notifications.models import APNSDevice, GCMDevice
|
||||
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.mobile_app.alert_rendering import get_push_notification_message
|
||||
from apps.user_management.models import User
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
MAX_RETRIES = 1 if settings.DEBUG else 10
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES)
|
||||
def notify_user_async(user_pk, alert_group_pk, notification_policy_pk, critical):
|
||||
# avoid circular import
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=user_pk)
|
||||
except User.DoesNotExist:
|
||||
logger.warning(f"User {user_pk} does not exist")
|
||||
return
|
||||
|
||||
try:
|
||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||
except AlertGroup.DoesNotExist:
|
||||
logger.warning(f"Alert group {alert_group_pk} does not exist")
|
||||
return
|
||||
|
||||
try:
|
||||
notification_policy = UserNotificationPolicy.objects.get(pk=notification_policy_pk)
|
||||
except UserNotificationPolicy.DoesNotExist:
|
||||
logger.warning(f"User notification policy {notification_policy_pk} does not exist")
|
||||
return
|
||||
|
||||
# APNS is for notifying iOS devices, GCM for Android
|
||||
apns_devices_to_notify = APNSDevice.objects.filter(user=user)
|
||||
gcm_devices_to_notify = GCMDevice.objects.filter(user=user)
|
||||
|
||||
# create an error log in case user has no devices set up
|
||||
if not apns_devices_to_notify.exists() and not gcm_devices_to_notify.exists():
|
||||
UserNotificationPolicyLogRecord.objects.create(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
notification_policy=notification_policy,
|
||||
alert_group=alert_group,
|
||||
reason="Mobile push notification error",
|
||||
notification_step=notification_policy.step,
|
||||
notification_channel=notification_policy.notify_by,
|
||||
)
|
||||
logger.info(f"Error while sending a mobile push notification: user {user_pk} has no devices set up")
|
||||
return
|
||||
|
||||
message = get_push_notification_message(alert_group)
|
||||
thread_id = f"{alert_group.channel.organization.public_primary_key}:{alert_group.public_primary_key}"
|
||||
|
||||
if critical:
|
||||
aps = {
|
||||
"alert": f"Critical page: {message}",
|
||||
"interruption-level": "critical",
|
||||
"sound": "ambulance.aiff",
|
||||
}
|
||||
else:
|
||||
aps = {
|
||||
"alert": message,
|
||||
"sound": "bingbong.aiff",
|
||||
}
|
||||
|
||||
apns_devices_to_notify.send_message(
|
||||
message,
|
||||
thread_id=thread_id,
|
||||
category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP
|
||||
extra={
|
||||
"orgId": alert_group.channel.organization.public_primary_key,
|
||||
"orgName": alert_group.channel.organization.stack_slug,
|
||||
"alertGroupId": alert_group.public_primary_key,
|
||||
"incidentId": alert_group.public_primary_key, # TODO: remove after hackathon app is deprecated
|
||||
"status": alert_group.status,
|
||||
"aps": aps,
|
||||
},
|
||||
)
|
||||
|
||||
gcm_devices_to_notify.send_message(
|
||||
message,
|
||||
thread_id=thread_id,
|
||||
category="USER_NEW_INCIDENT", # TODO: rename to USER_NEW_ALERT_GROUP
|
||||
extra={
|
||||
"orgId": alert_group.channel.organization.public_primary_key,
|
||||
"orgName": alert_group.channel.organization.stack_slug,
|
||||
"alertGroupId": alert_group.public_primary_key,
|
||||
"status": alert_group.status,
|
||||
"aps": aps,
|
||||
},
|
||||
)
|
||||
9
engine/apps/mobile_app/urls.py
Normal file
9
engine/apps/mobile_app/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from apps.mobile_app.views import APNSDeviceAuthorizedViewSet, GCMDeviceAuthorizedViewSet
|
||||
from common.api_helpers.optional_slash_router import OptionalSlashRouter
|
||||
|
||||
router = OptionalSlashRouter()
|
||||
|
||||
router.register("apns", APNSDeviceAuthorizedViewSet)
|
||||
router.register("gcm", GCMDeviceAuthorizedViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
12
engine/apps/mobile_app/views.py
Normal file
12
engine/apps/mobile_app/views.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet as BaseAPNSDeviceAuthorizedViewSet
|
||||
from push_notifications.api.rest_framework import GCMDeviceAuthorizedViewSet as BaseGCMDeviceAuthorizedViewSet
|
||||
|
||||
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
|
||||
|
||||
|
||||
class APNSDeviceAuthorizedViewSet(BaseAPNSDeviceAuthorizedViewSet):
|
||||
authentication_classes = (MobileAppAuthTokenAuthentication,)
|
||||
|
||||
|
||||
class GCMDeviceAuthorizedViewSet(BaseGCMDeviceAuthorizedViewSet):
|
||||
authentication_classes = (MobileAppAuthTokenAuthentication,)
|
||||
|
|
@ -51,6 +51,12 @@ if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
|||
path("slack/", include("apps.slack.urls")),
|
||||
]
|
||||
|
||||
if settings.FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
|
||||
urlpatterns += [
|
||||
path("mobile_app/", include("apps.mobile_app.urls")),
|
||||
]
|
||||
|
||||
|
||||
if settings.OSS_INSTALLATION:
|
||||
urlpatterns += [
|
||||
path("api/internal/v1/", include("apps.oss_installation.urls")),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ FEATURE_LIVE_SETTINGS_ENABLED = getenv_boolean("FEATURE_LIVE_SETTINGS_ENABLED",
|
|||
FEATURE_TELEGRAM_INTEGRATION_ENABLED = getenv_boolean("FEATURE_TELEGRAM_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_EMAIL_INTEGRATION_ENABLED = getenv_boolean("FEATURE_EMAIL_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_MOBILE_APP_INTEGRATION_ENABLED = getenv_boolean("FEATURE_MOBILE_APP_INTEGRATION_ENABLED", default=False)
|
||||
FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", default=False)
|
||||
FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", default=False)
|
||||
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
|
||||
|
|
@ -202,6 +203,7 @@ INSTALLED_APPS = [
|
|||
"apps.slack",
|
||||
"apps.telegram",
|
||||
"apps.twilioapp",
|
||||
"apps.mobile_app",
|
||||
"apps.api",
|
||||
"apps.api_for_grafana_incident",
|
||||
"apps.base",
|
||||
|
|
@ -540,9 +542,16 @@ GRAFANA_COM_ADMIN_API_TOKEN = os.environ.get("GRAFANA_COM_ADMIN_API_TOKEN", None
|
|||
|
||||
GRAFANA_API_KEY_NAME = "Grafana OnCall"
|
||||
|
||||
MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED = getenv_boolean("MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED", default=False)
|
||||
EXTRA_MESSAGING_BACKENDS = []
|
||||
if FEATURE_MOBILE_APP_INTEGRATION_ENABLED:
|
||||
EXTRA_MESSAGING_BACKENDS += [
|
||||
("apps.mobile_app.backend.MobileAppBackend", 5),
|
||||
("apps.mobile_app.backend.MobileAppCriticalBackend", 6),
|
||||
]
|
||||
|
||||
PUSH_NOTIFICATIONS_SETTINGS = {
|
||||
"FCM_API_KEY": os.environ.get("FCM_API_KEY", None),
|
||||
"GCM_API_KEY": os.environ.get("GCM_API_KEY", None),
|
||||
"APNS_AUTH_KEY_PATH": os.environ.get("APNS_AUTH_KEY_PATH", None),
|
||||
"APNS_TOPIC": os.environ.get("APNS_TOPIC", None),
|
||||
"APNS_AUTH_KEY_ID": os.environ.get("APNS_AUTH_KEY_ID", None),
|
||||
|
|
@ -569,8 +578,6 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = getenv_integer("DATA_UPLOAD_MAX_MEMORY_SIZE", 1_04
|
|||
# Log inbound/outbound calls as slow=1 if they exceed threshold
|
||||
SLOW_THRESHOLD_SECONDS = 2.0
|
||||
|
||||
EXTRA_MESSAGING_BACKENDS = []
|
||||
|
||||
# Email messaging backend
|
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||
EMAIL_HOST = os.getenv("EMAIL_HOST")
|
||||
|
|
@ -581,7 +588,7 @@ EMAIL_USE_TLS = getenv_boolean("EMAIL_USE_TLS", True)
|
|||
EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS")
|
||||
|
||||
if FEATURE_EMAIL_INTEGRATION_ENABLED:
|
||||
EXTRA_MESSAGING_BACKENDS = [("apps.email.backend.EmailBackend", 8)]
|
||||
EXTRA_MESSAGING_BACKENDS += [("apps.email.backend.EmailBackend", 8)]
|
||||
|
||||
INSTALLED_ONCALL_INTEGRATIONS = [
|
||||
"config_integrations.alertmanager",
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.integrations.tasks.create_alert": {"queue": "critical"},
|
||||
"apps.integrations.tasks.create_alertmanager_alerts": {"queue": "critical"},
|
||||
"apps.integrations.tasks.start_notify_about_integration_ratelimit": {"queue": "critical"},
|
||||
"apps.mobile_app.tasks.notify_user_async": {"queue": "critical"},
|
||||
"apps.schedules.tasks.drop_cached_ical.drop_cached_ical_for_custom_events_for_organization": {"queue": "critical"},
|
||||
"apps.schedules.tasks.drop_cached_ical.drop_cached_ical_task": {"queue": "critical"},
|
||||
# LONG
|
||||
|
|
|
|||
|
|
@ -34,26 +34,19 @@ const MobileAppVerification = observer((props: MobileAppVerificationProps) => {
|
|||
const [isMobileAppVerificationTokenExisting, setIsMobileAppVerificationTokenExisting] = useState<boolean>(false);
|
||||
const [MobileAppVerificationTokenLoading, setMobileAppVerificationTokenLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
userStore
|
||||
.getMobileAppVerificationToken(userPk)
|
||||
.then((_res) => {
|
||||
setIsMobileAppVerificationTokenExisting(true);
|
||||
setMobileAppVerificationTokenLoading(false);
|
||||
})
|
||||
.catch((_res) => {
|
||||
setIsMobileAppVerificationTokenExisting(false);
|
||||
setMobileAppVerificationTokenLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCreateMobileAppVerificationToken = async () => {
|
||||
setIsMobileAppVerificationTokenExisting(true);
|
||||
await userStore
|
||||
.createMobileAppVerificationToken(userPk)
|
||||
.then((res) => setShowMobileAppVerificationToken(res?.token));
|
||||
.sendBackendConfirmationCode(userPk, 'MOBILE_APP')
|
||||
.then((res) => setShowMobileAppVerificationToken(res));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleCreateMobileAppVerificationToken().then(() => {
|
||||
setMobileAppVerificationTokenLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cx('mobile-app-settings')}>
|
||||
{MobileAppVerificationTokenLoading ? (
|
||||
|
|
|
|||
|
|
@ -378,16 +378,4 @@ export class UserStore extends BaseStore {
|
|||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
async getMobileAppVerificationToken(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/mobile_app_verification_token/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
async createMobileAppVerificationToken(userPk: User['pk']) {
|
||||
return await makeRequest(`/users/${userPk}/mobile_app_verification_token/`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue