# Which issue(s) this PR fixes
Solves the (rare) issue where a user could potentially have > 1
mobileapp auth token, leading to 500 errors when trying to interact w/
the authtoken (ex. disconnect a mobile app from a user's profile):
```shell
2023-03-07 10:12:13 source=engine:app google_trace_id=e14bf933d634068a48caf093ce43c7f5/5550677047491218352 logger=django.request Internal Server Error: /api/internal/v1/users/U6WJ3BRLM1TR7/unlink_backend
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/etc/app/apps/api/views/user.py", line 453, in unlink_backend
backend.unlink_user(user)
File "/etc/app/apps/mobile_app/backend.py", line 34, in unlink_user
token = MobileAppAuthToken.objects.get(user=user)
File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
raise self.model.MultipleObjectsReturned(
apps.mobile_app.models.MobileAppAuthToken.MultipleObjectsReturned: get() returned more than one MobileAppAuthToken -- it returned 2!
```
## Checklist
- [x] Tests updated
- [ ] Documentation added (N/A)
- [x] `CHANGELOG.md` updated
70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
from typing import Tuple
|
|
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
|
|
from apps.auth_token import constants, crypto
|
|
from apps.auth_token.models import BaseAuthToken
|
|
from apps.user_management.models import Organization, User
|
|
|
|
MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS = 60 * (5 if settings.DEBUG else 1)
|
|
|
|
|
|
def get_expire_date():
|
|
return timezone.now() + timezone.timedelta(seconds=MOBILE_APP_AUTH_VERIFICATION_TOKEN_TIMEOUT_SECONDS)
|
|
|
|
|
|
class MobileAppVerificationTokenQueryset(models.QuerySet):
|
|
def filter(self, *args, **kwargs):
|
|
now = timezone.now()
|
|
return super().filter(*args, **kwargs, revoked_at=None, expire_date__gte=now)
|
|
|
|
def delete(self):
|
|
self.update(revoked_at=timezone.now())
|
|
|
|
|
|
class MobileAppVerificationToken(BaseAuthToken):
|
|
objects = MobileAppVerificationTokenQueryset.as_manager()
|
|
user = models.ForeignKey(
|
|
"user_management.User",
|
|
related_name="mobile_app_verification_token_set",
|
|
on_delete=models.CASCADE,
|
|
)
|
|
organization = models.ForeignKey(
|
|
"user_management.Organization", related_name="mobile_app_verification_token_set", on_delete=models.CASCADE
|
|
)
|
|
expire_date = models.DateTimeField(default=get_expire_date)
|
|
|
|
@classmethod
|
|
def create_auth_token(cls, user: User, organization: Organization) -> Tuple["MobileAppVerificationToken", str]:
|
|
token_string = crypto.generate_short_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
|
|
|
|
|
|
class MobileAppAuthToken(BaseAuthToken):
|
|
user = models.OneToOneField(to=User, null=False, blank=False, 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
|