From 85fe13edcf02729c639975278a8dc3e01587b0db Mon Sep 17 00:00:00 2001 From: Klaus-Uwe Mitterer Date: Fri, 22 May 2020 18:13:23 +0200 Subject: [PATCH] Implemented TOTP Implemented currencies and taxes --- core/classes/otp.py | 4 +++ core/helpers/otp.py | 9 +------ core/models/__init__.py | 3 ++- core/models/auth.py | 4 --- core/models/local.py | 34 +++++++++++++++++++++++++ core/views/auth.py | 8 +++--- expephalon/settings.py | 3 ++- requirements.txt | 2 ++ totp/management/commands/disabletotp.py | 22 ++++++++++++++++ totp/management/commands/enabletotp.py | 32 +++++++++++++++++++++++ totp/models.py | 4 +-- totp/otp.py | 9 ++++++- totp/requirements.txt | 1 + 13 files changed, 112 insertions(+), 23 deletions(-) create mode 100644 core/models/local.py create mode 100644 totp/management/commands/disabletotp.py create mode 100644 totp/management/commands/enabletotp.py diff --git a/core/classes/otp.py b/core/classes/otp.py index bd7140a..aa3fd86 100644 --- a/core/classes/otp.py +++ b/core/classes/otp.py @@ -17,6 +17,10 @@ class BaseOTPProvider: '''Returns True if the provider is properly configured and ready to use.''' raise NotImplementedError(f"{type(self)} does not implement is_active!") + def active_for_user(self, user): + '''Returns True if the provider is active and ready to be used by user.''' + return self.is_active + def start_authentication(self, user): return "Authentication started, please enter your 2FA token." diff --git a/core/helpers/otp.py b/core/helpers/otp.py index d1be561..f021a55 100644 --- a/core/helpers/otp.py +++ b/core/helpers/otp.py @@ -1,17 +1,10 @@ -from core.models import OTPUser from core.modules.otp import providers def get_user_otps(user): - try: - all_otps = OTPUser.objects.filter(user=user) - except: - return {} - - user_providers = [otp.provider for otp in all_otps] active_providers = {} for name, provider in providers.items(): - if name in user_providers: + if provider().active_for_user(user): active_providers[name] = provider return active_providers diff --git a/core/models/__init__.py b/core/models/__init__.py index b73eae0..4dc2b3b 100644 --- a/core/models/__init__.py +++ b/core/models/__init__.py @@ -1,3 +1,4 @@ from core.models.files import * from core.models.profiles import * -from core.models.auth import * \ No newline at end of file +from core.models.auth import * +from core.models.local import * \ No newline at end of file diff --git a/core/models/auth.py b/core/models/auth.py index f763f1d..2b82feb 100644 --- a/core/models/auth.py +++ b/core/models/auth.py @@ -3,10 +3,6 @@ from django.contrib.auth import get_user_model from uuid import uuid4 -class OTPUser(Model): - user = ForeignKey(get_user_model(), CASCADE) - provider = CharField(max_length=255) - class LoginSession(Model): uuid = UUIDField(default=uuid4, primary_key=True) user = ForeignKey(get_user_model(), CASCADE) diff --git a/core/models/local.py b/core/models/local.py new file mode 100644 index 0000000..157362b --- /dev/null +++ b/core/models/local.py @@ -0,0 +1,34 @@ +from django.db.models import Model, CharField, BooleanField, DecimalField, ForeignKey, CASCADE + +from django_countries.fields import CountryField + +class Currency(Model): + name = CharField(max_length=255, unique=True) + code = CharField(max_length=16, unique=True) + symbol = CharField(max_length=8) + base = BooleanField(default=False) + rate = DecimalField(default=1, max_digits=30, decimal_places=10) + + def set_base(self): + type(self).get_base().update(base=False) + self.update(base=True) + + @classmethod + def get_base(cls): + return cls.objects.get(base=True) + +class TaxPolicy(Model): + name = CharField(max_length=255, blank=True) + default_rate = DecimalField(default=0, max_digits=10, decimal_places=5) + + def get_applicable_rate(self, country, reverse_charge=False): + rule = self.taxrule_set.get(destination_country=country) + if reverse_charge: + return rule.tax_rate if not rule.reverse_charge else 0 + return rule.tax_rate + +class TaxRule(Model): + policy = ForeignKey(TaxPolicy, on_delete=CASCADE) + destination_country = CountryField() + tax_rate = DecimalField(max_digits=10, decimal_places=5) + reverse_charge = BooleanField(default=False) diff --git a/core/views/auth.py b/core/views/auth.py index e7235fd..ab18a81 100644 --- a/core/views/auth.py +++ b/core/views/auth.py @@ -166,8 +166,6 @@ class PWRequestView(FormView): token = PWResetToken.objects.create(user=user) mail = generate_pwreset_mail(user, token) simple_send_mail("Password Reset", mail, user.email) - except: - raise -# finally: -# messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.") -# return redirect("login") \ No newline at end of file + finally: + messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.") + return redirect("login") \ No newline at end of file diff --git a/expephalon/settings.py b/expephalon/settings.py index 6cdd7b6..aba5ec2 100644 --- a/expephalon/settings.py +++ b/expephalon/settings.py @@ -22,6 +22,7 @@ INSTALLED_APPS = [ 'dbsettings', 'django_celery_results', 'django_celery_beat', + 'django_countries', ] + EXPEPHALON_MODULES MIDDLEWARE = [ @@ -153,4 +154,4 @@ CELERY_TASK_RESULT_EXPIRES = 12 * 60 * 60 LOGIN_REDIRECT_URL = reverse_lazy('dashboard') LOGIN_URL = reverse_lazy('login') -LOGOUT_URL = reverse_lazy('logout') \ No newline at end of file +LOGOUT_URL = reverse_lazy('logout') diff --git a/requirements.txt b/requirements.txt index 140a2c3..85795ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ celery django-celery-results django-celery-beat python-memcached +django-countries +pyuca diff --git a/totp/management/commands/disabletotp.py b/totp/management/commands/disabletotp.py new file mode 100644 index 0000000..c2f7d36 --- /dev/null +++ b/totp/management/commands/disabletotp.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth import get_user_model + +from totp.models import TOTPUser +from totp.otp import TOTP + +class Command(BaseCommand): + help = 'Disables TOTP for the specified user (identified by username)' + + def add_arguments(self, parser): + parser.add_argument('user', type=str) + + def handle(self, *args, **options): + try: + user = get_user_model().objects.get(username=options["user"]) + except get_user_model().DoesNotExist: + raise ValueError(f"User {options['user']} does not exist") + + try: + TOTPUser.objects.get(user=user).delete() + except TOTPUser.DoesNotExist: + raise ValueError(f"TOTP not enabled for user {options['user']}") diff --git a/totp/management/commands/enabletotp.py b/totp/management/commands/enabletotp.py new file mode 100644 index 0000000..24ce22a --- /dev/null +++ b/totp/management/commands/enabletotp.py @@ -0,0 +1,32 @@ +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth import get_user_model + +from totp.models import TOTPUser +from totp.otp import TOTP + +from dbsettings.functions import getValue + +import pyqrcode +import pyotp + +class Command(BaseCommand): + help = 'Enables TOTP for the specified user (identified by username)' + + def add_arguments(self, parser): + parser.add_argument('user', type=str) + + def handle(self, *args, **options): + try: + user = get_user_model().objects.get(username=options["user"]) + except get_user_model().DoesNotExist: + raise ValueError(f"User {options['user']} does not exist") + + if TOTP().active_for_user(user): + raise ValueError(f"TOTP already enabled for user {options['user']}") + + totu = TOTPUser.objects.create(user=user) + + uri = pyotp.totp.TOTP(totu.secret).provisioning_uri(options["user"], issuer_name=getValue("core.title", "Expephalon")) + + print(pyqrcode.create(uri).terminal()) + print(uri) \ No newline at end of file diff --git a/totp/models.py b/totp/models.py index f135ed5..b880a7f 100644 --- a/totp/models.py +++ b/totp/models.py @@ -5,8 +5,6 @@ from dbsettings.functions import getValue import pyotp -# Create your models here. - class TOTPUser(Model): - secret = CharField(max_length=32, default=pyotp.random_base32()) + secret = CharField(max_length=32, default=pyotp.random_base32) user = ForeignKey(get_user_model(), CASCADE) diff --git a/totp/otp.py b/totp/otp.py index 89168a3..e8b15f9 100644 --- a/totp/otp.py +++ b/totp/otp.py @@ -16,6 +16,13 @@ class TOTP(BaseOTPProvider): def is_active(self): return True + def active_for_user(self, user): + try: + TOTPUser.objects.get(user=user) + return super().active_for_user(user) + except TOTPUser.DoesNotExist: + return False + def start_authentication(self, user): return "Please enter the token displayed in your app." @@ -23,7 +30,7 @@ class TOTP(BaseOTPProvider): try: otpuser = TOTPUser.objects.get(user=user) return pyotp.TOTP(otpuser.secret).verify(token) - except OTPUser.DoesNotExist: + except TOTPUser.DoesNotExist: return False OTPPROVIDERS = {"totp": TOTP} \ No newline at end of file diff --git a/totp/requirements.txt b/totp/requirements.txt index 6c1907d..92138ba 100644 --- a/totp/requirements.txt +++ b/totp/requirements.txt @@ -1 +1,2 @@ pyotp +pyqrcode \ No newline at end of file