Skip to content

Commit

Permalink
[feature] Added API endpoint to get active PhoneToken status
Browse files Browse the repository at this point in the history
  • Loading branch information
pandafy authored Jun 26, 2023
1 parent cbedf55 commit 0abe24f
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 1 deletion.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ jobs:
- name: Install openwisp-radius
run: |
pip install -e .[saml,openvpn_status]
pip install "openwisp-utils[rest,celery] @ https://github.com/openwisp/openwisp-utils/tarball/master"
pip install ${{ matrix.django-version }}
- name: QA checks
Expand Down
21 changes: 21 additions & 0 deletions docs/source/user/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ To override the default API throttling settings, add the following to your ``set
'obtain_auth_token': None,
'validate_auth_token': None,
'create_phone_token': None,
'phone_token_status': None,
'validate_phone_token': None,
# Relaxed throttling Policy
'others': '400/hour',
Expand Down Expand Up @@ -674,6 +675,26 @@ Responds only to **POST**.

No parameters required.

Get active SMS token status
---------------------------

.. note::
This API endpoint will work only if the organization has enabled
:ref:`SMS verification <openwisp_radius_sms_verification_enabled>`.

**Requires the user auth token (Bearer Token)**.

Used for SMS verification, allows checking whether an active SMS token was
already requested for the mobile phone number of the logged in account.

.. code-block:: text
/api/v1/radius/organization/<organization-slug>/account/phone/token/active/
Responds only to **GET**.

No parameters required.

.. _verify_validate_sms_token:

Verify/Validate SMS token
Expand Down
5 changes: 5 additions & 0 deletions openwisp_radius/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ def get_api_urls(api_views=None):
api_views.create_phone_token,
name='phone_token_create',
),
path(
'radius/organization/<slug:slug>/account/phone/token/active/',
api_views.get_phone_token_status,
name='phone_token_status',
),
path(
'radius/organization/<slug:slug>/account/phone/verify/',
api_views.validate_phone_token,
Expand Down
38 changes: 38 additions & 0 deletions openwisp_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,44 @@ def create(self, *args, **kwargs):
create_phone_token = CreatePhoneTokenView.as_view()


class GetPhoneTokenStatusView(DispatchOrgMixin, GenericAPIView):
throttle_scope = 'phone_token_status'
authentication_classes = (BearerAuthentication,)
permission_classes = (
IsSmsVerificationEnabled,
IsAuthenticated,
)
serializer_class = serializers.Serializer

@swagger_auto_schema(
operation_description=(
"""
**Requires the user auth token (Bearer Token).**
Used for SMS verification, allows checking whether an active
SMS token was already requested for the mobile phone number
of the logged in account.
"""
),
responses={200: '`{"active":"true/false"}`'},
)
def get(self, request, *args, **kwargs):
user = request.user
self.validate_membership(user)
is_active = PhoneToken.objects.filter(
user=request.user,
phone_number=user.phone_number,
valid_until__gte=timezone.now(),
verified=False,
).exists()
return Response(
data={'active': is_active},
status=200,
)


get_phone_token_status = GetPhoneTokenStatusView.as_view()


class ValidatePhoneTokenView(DispatchOrgMixin, GenericAPIView):
throttle_scope = 'validate_phone_token'
authentication_classes = (BearerAuthentication,)
Expand Down
1 change: 1 addition & 0 deletions openwisp_radius/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class OpenwispRadiusConfig(ApiAppConfig):
'validate_auth_token': None,
'create_phone_token': None,
'validate_phone_token': None,
'phone_token_status': None,
# Relaxed throttling Policy
'others': default_or_test('400/hour', None),
},
Expand Down
54 changes: 54 additions & 0 deletions openwisp_radius/tests/test_api/test_phone_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.urls import reverse
from django.utils import timezone
from freezegun import freeze_time
from rest_framework.authtoken.models import Token

Expand Down Expand Up @@ -183,6 +184,59 @@ def test_create_phone_token_400_limit_reached(self):
else:
self.assertEqual(r.status_code, 201)

@capture_any_output()
def test_phone_token_status_401(self):
url = reverse('radius:phone_token_status', args=[self.default_org.slug])
r = self.client.get(url)
self.assertEqual(r.status_code, 401)

@capture_any_output()
def test_phone_token_status_200(self):
self._register_user()
token = Token.objects.last()
url = reverse('radius:phone_token_status', args=[self.default_org.slug])
with self.subTest('Test no PhoneToken present'):
response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {token.key}')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'active': False})

# Create PhoneToken
response = self.client.post(
reverse('radius:phone_token_create', args=[self.default_org.slug]),
HTTP_AUTHORIZATION=f'Bearer {token.key}',
)
self.assertEqual(response.status_code, 201)

with self.subTest('Test valid PhoneToken present'):
response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {token.key}')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'active': True})

with self.subTest('Test expired PhoneToken present'):
with freeze_time(timezone.now() + timedelta(days=1)):
response = self.client.get(
url, HTTP_AUTHORIZATION=f'Bearer {token.key}'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'active': False})

with self.subTest('Test PhoneToken already verified'):
PhoneToken.objects.update(verified=True)
response = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {token.key}')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {'active': False})

@capture_any_output()
def test_phone_token_status_400_not_member(self):
self.test_create_phone_token_201()
OrganizationUser.objects.all().delete()
token = Token.objects.last()
url = reverse('radius:phone_token_status', args=[self.default_org.slug])
r = self.client.get(url, HTTP_AUTHORIZATION=f'Bearer {token.key}')
self.assertEqual(r.status_code, 400)
self.assertIn('non_field_errors', r.data)
self.assertIn('is not member', str(r.data['non_field_errors']))

@freeze_time(_TEST_DATE)
@capture_any_output()
def test_validate_phone_token_200(self):
Expand Down
8 changes: 8 additions & 0 deletions tests/openwisp2/sample_radius/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from openwisp_radius.api.views import (
DownloadRadiusBatchPdfView as BaseDownloadRadiusBatchPdfView,
)
from openwisp_radius.api.views import (
GetPhoneTokenStatusView as BaseGetPhoneTokenStatusView,
)
from openwisp_radius.api.views import ObtainAuthTokenView as BaseObtainAuthTokenView
from openwisp_radius.api.views import PasswordChangeView as BasePasswordChangeView
from openwisp_radius.api.views import (
Expand Down Expand Up @@ -69,6 +72,10 @@ class CreatePhoneTokenView(BaseCreatePhoneTokenView):
pass


class GetPhoneTokenStatusView(BaseGetPhoneTokenStatusView):
pass


class ValidatePhoneTokenView(BaseValidatePhoneTokenView):
pass

Expand All @@ -93,6 +100,7 @@ class DownloadRadiusBatchPdfView(BaseDownloadRadiusBatchPdfView):
password_reset = PasswordResetView.as_view()
password_reset_confirm = PasswordResetConfirmView.as_view()
create_phone_token = CreatePhoneTokenView.as_view()
get_phone_token_status = GetPhoneTokenStatusView.as_view()
validate_phone_token = ValidatePhoneTokenView.as_view()
change_phone_number = ChangePhoneNumberView.as_view()
download_rad_batch_pdf = DownloadRadiusBatchPdfView.as_view()

0 comments on commit 0abe24f

Please sign in to comment.