User profile models
SMS OTP provider Login template (without css)
This commit is contained in:
parent
9798849aab
commit
d74a4c1b8b
20 changed files with 183 additions and 23 deletions
21
core/classes/otp.py
Normal file
21
core/classes/otp.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
class BaseOTPProvider:
|
||||||
|
'''OTP providers must be subclasses of BaseOTPProvider and implement at least validate_token().'''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_name(self):
|
||||||
|
return "Base OTP Provider"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_logo(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
'''Returns True if the provider is properly configured and ready to use.'''
|
||||||
|
raise NotImplementedError(f"{type(self)} does not implement is_active!")
|
||||||
|
|
||||||
|
def start_authentication(self, user):
|
||||||
|
return "Authentication started, please enter your 2FA token."
|
||||||
|
|
||||||
|
def validate_token(self, user, token):
|
||||||
|
raise NotImplementedError(f"{type(self)} does not implement validate_token()!")
|
|
@ -1 +1,2 @@
|
||||||
from core.models.files import *
|
from core.models.files import *
|
||||||
|
from core.models.profiles import *
|
12
core/models/profiles.py
Normal file
12
core/models/profiles.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from polymorphic.models import PolymorphicModel
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
|
||||||
|
from django.db.models import OneToOneField, CASCADE, CharField
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
class Profile(PolymorphicModel):
|
||||||
|
user = OneToOneField(get_user_model(), CASCADE)
|
||||||
|
mobile = PhoneNumberField(blank=True)
|
||||||
|
|
||||||
|
class AdminProfile(Profile):
|
||||||
|
role = CharField(max_length=64)
|
13
core/modules/otp.py
Normal file
13
core/modules/otp.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
providers = []
|
||||||
|
|
||||||
|
for module in settings.EXPEPHALON_MODULES:
|
||||||
|
try:
|
||||||
|
moo = importlib.import_module(f"{module}.otp")
|
||||||
|
for provider in moo.OTPPROVIDERS:
|
||||||
|
providers.append(provider)
|
||||||
|
except (AttributeError, ModuleNotFoundError):
|
||||||
|
continue
|
|
@ -2,9 +2,7 @@ import importlib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from core.classes.navigation import NavItem, NavSection, Navigation
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
from dbsettings import getValue
|
|
||||||
|
|
||||||
providers = []
|
providers = []
|
||||||
|
|
||||||
|
@ -16,7 +14,7 @@ for module in settings.EXPEPHALON_MODULES:
|
||||||
for provider in mos.SMSPROVIDERS:
|
for provider in mos.SMSPROVIDERS:
|
||||||
providers.append(provider)
|
providers.append(provider)
|
||||||
if mos.CREATE:
|
if mos.CREATE:
|
||||||
modules_available = mos.CREATE
|
modules_available.append(mos.CREATE)
|
||||||
except (AttributeError, ModuleNotFoundError):
|
except (AttributeError, ModuleNotFoundError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,7 @@ administration_section = NavSection("Administration", "")
|
||||||
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "backend")
|
user_administration_item = NavItem("Administrator Users", "fa-users-cog", "backend")
|
||||||
brand_administration_item = NavItem("Brands", "fa-code-branch", "backend")
|
brand_administration_item = NavItem("Brands", "fa-code-branch", "backend")
|
||||||
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backend")
|
sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backend")
|
||||||
|
otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backend")
|
||||||
backup_administration_item = NavItem("Backups", "fa-shield-alt", "backend")
|
backup_administration_item = NavItem("Backups", "fa-shield-alt", "backend")
|
||||||
product_administration_item = NavItem("Products", "fa-cube", "backend")
|
product_administration_item = NavItem("Products", "fa-cube", "backend")
|
||||||
pgroup_administration_item = NavItem("Product Groups", "fa-cubes", "backend")
|
pgroup_administration_item = NavItem("Product Groups", "fa-cubes", "backend")
|
||||||
|
@ -100,6 +101,7 @@ dbsettings_item = NavItem("Database Settings", "fa-database", "dbsettings")
|
||||||
administration_section.add_item(user_administration_item)
|
administration_section.add_item(user_administration_item)
|
||||||
administration_section.add_item(brand_administration_item)
|
administration_section.add_item(brand_administration_item)
|
||||||
administration_section.add_item(sms_administration_item)
|
administration_section.add_item(sms_administration_item)
|
||||||
|
administration_section.add_item(otp_administration_item)
|
||||||
administration_section.add_item(backup_administration_item)
|
administration_section.add_item(backup_administration_item)
|
||||||
administration_section.add_item(product_administration_item)
|
administration_section.add_item(product_administration_item)
|
||||||
administration_section.add_item(pgroup_administration_item)
|
administration_section.add_item(pgroup_administration_item)
|
||||||
|
|
|
@ -1,15 +1,3 @@
|
||||||
"""
|
|
||||||
Django settings for expephalon project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 3.0.4.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from expephalon.custom_settings import * # pylint: disable=unused-wildcard-import
|
from expephalon.custom_settings import * # pylint: disable=unused-wildcard-import
|
||||||
|
@ -26,6 +14,8 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'polymorphic',
|
||||||
|
'phonenumber_field',
|
||||||
'core',
|
'core',
|
||||||
'dbsettings',
|
'dbsettings',
|
||||||
] + EXPEPHALON_MODULES
|
] + EXPEPHALON_MODULES
|
||||||
|
|
|
@ -2,3 +2,7 @@
|
||||||
|
|
||||||
* core
|
* core
|
||||||
|
|
||||||
|
### smsotp
|
||||||
|
|
||||||
|
* core
|
||||||
|
* any SMS provider (e.g. playsms)
|
|
@ -25,11 +25,11 @@ class PlaySMSServer(Model, BaseSMSProvider):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getError(status):
|
def getError(status):
|
||||||
if status["error_message"]:
|
if status["error_string"]:
|
||||||
return status["error_message"]
|
return status["error_string"]
|
||||||
try:
|
try:
|
||||||
if status["data"]["error"]:
|
if int(status["data"][0]["error"]):
|
||||||
return status["data"]["error"]
|
return int(status["data"][0]["error"])
|
||||||
finally:
|
finally:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -43,11 +43,13 @@ class PlaySMSServer(Model, BaseSMSProvider):
|
||||||
if isinstance(recipients, list):
|
if isinstance(recipients, list):
|
||||||
recipients = ",".join(recipients)
|
recipients = ",".join(recipients)
|
||||||
|
|
||||||
url = 'http%s://%s/index.php?app=ws&u="%s"&h="%s"&op=pv&to=%s&msg=%s' % ("s" if self.https else "", self.host, self.username, self.token, recipients, quote_plus(message))
|
url = 'http%s://%s/index.php?app=ws&u=%s&h=%s&op=pv&to=%s&msg=%s' % ("s" if self.https else "", self.host, self.username, self.token, recipients, quote_plus(message))
|
||||||
|
|
||||||
|
print(url)
|
||||||
response = urlopen(url)
|
response = urlopen(url)
|
||||||
|
|
||||||
status = json.loads(response.read().decode())
|
status = json.loads(response.read().decode())
|
||||||
|
print(status)
|
||||||
|
|
||||||
error = PlaySMSServer.getError(status)
|
error = PlaySMSServer.getError(status)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from playsms.models import PlaySMSServer
|
||||||
|
|
||||||
|
SMSPROVIDERS = list(PlaySMSServer.objects.all())
|
|
@ -3,3 +3,5 @@ mysqlclient
|
||||||
django-storages
|
django-storages
|
||||||
boto3
|
boto3
|
||||||
Pillow
|
Pillow
|
||||||
|
django-polymorphic
|
||||||
|
django-phonenumber-field[phonenumbers]
|
||||||
|
|
0
smsotp/__init__.py
Normal file
0
smsotp/__init__.py
Normal file
3
smsotp/admin.py
Normal file
3
smsotp/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
5
smsotp/apps.py
Normal file
5
smsotp/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SmsotpConfig(AppConfig):
|
||||||
|
name = 'smsotp'
|
6
smsotp/helpers.py
Normal file
6
smsotp/helpers.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
def generate_token(len=getValue("smsotp.length", 6), chars=getValue("smsotp.chars", "0123456789")):
|
||||||
|
return "".join(random.choices(chars, k=int(len)))
|
26
smsotp/models.py
Normal file
26
smsotp/models.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
from django.db.models import Model, CharField, ForeignKey, CASCADE, DateTimeField
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
from smsotp.helpers import generate_token
|
||||||
|
from core.modules.sms import get_default_sms_provider
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class OTPToken(Model):
|
||||||
|
token = CharField(max_length=32, default=generate_token)
|
||||||
|
user = ForeignKey(get_user_model(), CASCADE)
|
||||||
|
creation = DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def send_sms(self, text=getValue("smsotp.text", "Your authentication token is:"), number=None):
|
||||||
|
if "_TOKEN_" in text:
|
||||||
|
text = text.replace("_TOKEN_", self.token)
|
||||||
|
else:
|
||||||
|
text = f"{text} {self.token}"
|
||||||
|
|
||||||
|
number = number or self.user.profile.mobile
|
||||||
|
|
||||||
|
provider = get_default_sms_provider()
|
||||||
|
|
||||||
|
provider.sendSMS(number, text)
|
40
smsotp/otp.py
Normal file
40
smsotp/otp.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from core.classes.otp import BaseOTPProvider
|
||||||
|
from smsotp.models import OTPToken
|
||||||
|
from core.modules.sms import get_default_sms_provider
|
||||||
|
|
||||||
|
from dbsettings.functions import getValue
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
class SMSOTP(BaseOTPProvider):
|
||||||
|
@property
|
||||||
|
def get_name(self):
|
||||||
|
return "SMS OTP"
|
||||||
|
|
||||||
|
def create_token(self, user):
|
||||||
|
token = OTPToken.objects.create(user=user)
|
||||||
|
try:
|
||||||
|
token.send_sms()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
return bool(get_default_sms_provider())
|
||||||
|
|
||||||
|
def start_authentication(self, user):
|
||||||
|
if self.create_token(user):
|
||||||
|
return "We have sent you an SMS containing your authentication token."
|
||||||
|
else:
|
||||||
|
return "An error has occurred, please try again later or contact the administrator."
|
||||||
|
|
||||||
|
def validate_token(self, user, token):
|
||||||
|
try:
|
||||||
|
max_age = timezone.now() - timezone.timedelta(seconds=int(getValue("smsotp.max_age", "300")))
|
||||||
|
OTPToken.objects.get(user=user, token=token, creation__gte=max_age).delete()
|
||||||
|
return True
|
||||||
|
except OTPToken.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
OTPPROVIDERS = [SMSOTP]
|
3
smsotp/tests.py
Normal file
3
smsotp/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
smsotp/views.py
Normal file
3
smsotp/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
26
templates/backend/login.html
Normal file
26
templates/backend/login.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="{% static "backend/css/login.css">
|
||||||
|
<link href="https://fontproxy.kumi.systems/css?family=Ubuntu" rel="stylesheet">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="https://fa.kumi.systems/css/all.css">
|
||||||
|
<title>Sign in</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<p class="sign" align="center">Sign in</p>
|
||||||
|
{% csrf_token %}
|
||||||
|
<form class="form1">
|
||||||
|
<input class="un " type="text" align="center" name="username" placeholder="Username">
|
||||||
|
<input class="pass" type="password" align="center" name="password" placeholder="Password">
|
||||||
|
<a class="submit" align="center">Sign in</a>
|
||||||
|
<p class="forgot" align="center"><a href="#">Forgot Password?</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in a new issue