Implemented TOTP
Implemented currencies and taxes
This commit is contained in:
parent
89f971117f
commit
85fe13edcf
13 changed files with 112 additions and 23 deletions
|
@ -17,6 +17,10 @@ class BaseOTPProvider:
|
||||||
'''Returns True if the provider is properly configured and ready to use.'''
|
'''Returns True if the provider is properly configured and ready to use.'''
|
||||||
raise NotImplementedError(f"{type(self)} does not implement is_active!")
|
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):
|
def start_authentication(self, user):
|
||||||
return "Authentication started, please enter your 2FA token."
|
return "Authentication started, please enter your 2FA token."
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
from core.models import OTPUser
|
|
||||||
from core.modules.otp import providers
|
from core.modules.otp import providers
|
||||||
|
|
||||||
def get_user_otps(user):
|
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 = {}
|
active_providers = {}
|
||||||
|
|
||||||
for name, provider in providers.items():
|
for name, provider in providers.items():
|
||||||
if name in user_providers:
|
if provider().active_for_user(user):
|
||||||
active_providers[name] = provider
|
active_providers[name] = provider
|
||||||
|
|
||||||
return active_providers
|
return active_providers
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
from core.models.files import *
|
from core.models.files import *
|
||||||
from core.models.profiles import *
|
from core.models.profiles import *
|
||||||
from core.models.auth import *
|
from core.models.auth import *
|
||||||
|
from core.models.local import *
|
|
@ -3,10 +3,6 @@ from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
class OTPUser(Model):
|
|
||||||
user = ForeignKey(get_user_model(), CASCADE)
|
|
||||||
provider = CharField(max_length=255)
|
|
||||||
|
|
||||||
class LoginSession(Model):
|
class LoginSession(Model):
|
||||||
uuid = UUIDField(default=uuid4, primary_key=True)
|
uuid = UUIDField(default=uuid4, primary_key=True)
|
||||||
user = ForeignKey(get_user_model(), CASCADE)
|
user = ForeignKey(get_user_model(), CASCADE)
|
||||||
|
|
34
core/models/local.py
Normal file
34
core/models/local.py
Normal file
|
@ -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)
|
|
@ -166,8 +166,6 @@ class PWRequestView(FormView):
|
||||||
token = PWResetToken.objects.create(user=user)
|
token = PWResetToken.objects.create(user=user)
|
||||||
mail = generate_pwreset_mail(user, token)
|
mail = generate_pwreset_mail(user, token)
|
||||||
simple_send_mail("Password Reset", mail, user.email)
|
simple_send_mail("Password Reset", mail, user.email)
|
||||||
except:
|
finally:
|
||||||
raise
|
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.")
|
||||||
# finally:
|
return redirect("login")
|
||||||
# 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")
|
|
|
@ -22,6 +22,7 @@ INSTALLED_APPS = [
|
||||||
'dbsettings',
|
'dbsettings',
|
||||||
'django_celery_results',
|
'django_celery_results',
|
||||||
'django_celery_beat',
|
'django_celery_beat',
|
||||||
|
'django_countries',
|
||||||
] + EXPEPHALON_MODULES
|
] + EXPEPHALON_MODULES
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
@ -12,3 +12,5 @@ celery
|
||||||
django-celery-results
|
django-celery-results
|
||||||
django-celery-beat
|
django-celery-beat
|
||||||
python-memcached
|
python-memcached
|
||||||
|
django-countries
|
||||||
|
pyuca
|
||||||
|
|
22
totp/management/commands/disabletotp.py
Normal file
22
totp/management/commands/disabletotp.py
Normal file
|
@ -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']}")
|
32
totp/management/commands/enabletotp.py
Normal file
32
totp/management/commands/enabletotp.py
Normal file
|
@ -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)
|
|
@ -5,8 +5,6 @@ from dbsettings.functions import getValue
|
||||||
|
|
||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
|
||||||
class TOTPUser(Model):
|
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)
|
user = ForeignKey(get_user_model(), CASCADE)
|
||||||
|
|
|
@ -16,6 +16,13 @@ class TOTP(BaseOTPProvider):
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
return True
|
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):
|
def start_authentication(self, user):
|
||||||
return "Please enter the token displayed in your app."
|
return "Please enter the token displayed in your app."
|
||||||
|
|
||||||
|
@ -23,7 +30,7 @@ class TOTP(BaseOTPProvider):
|
||||||
try:
|
try:
|
||||||
otpuser = TOTPUser.objects.get(user=user)
|
otpuser = TOTPUser.objects.get(user=user)
|
||||||
return pyotp.TOTP(otpuser.secret).verify(token)
|
return pyotp.TOTP(otpuser.secret).verify(token)
|
||||||
except OTPUser.DoesNotExist:
|
except TOTPUser.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
OTPPROVIDERS = {"totp": TOTP}
|
OTPPROVIDERS = {"totp": TOTP}
|
|
@ -1 +1,2 @@
|
||||||
pyotp
|
pyotp
|
||||||
|
pyqrcode
|
Loading…
Reference in a new issue