Merge branch 'william' into 'master'
request the username in login instead of the email See merge request ungleich-public/ungleich-user!6
This commit is contained in:
commit
b7011b0574
14
dal/forms.py
14
dal/forms.py
|
@ -4,18 +4,18 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
|
||||
class LoginForm(forms.Form):
|
||||
email = forms.CharField(widget=forms.TextInput())
|
||||
username = forms.CharField(widget=forms.TextInput())
|
||||
password = forms.CharField(widget=forms.PasswordInput())
|
||||
|
||||
class Meta:
|
||||
fields = ['email', 'password']
|
||||
fields = ['username', 'password']
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
username = self.cleaned_data.get('username')
|
||||
password = self.cleaned_data.get('password')
|
||||
if self.errors:
|
||||
return self.cleaned_data
|
||||
is_auth = authenticate(username=email, password=password)
|
||||
is_auth = authenticate(username=username, password=password)
|
||||
if not is_auth:
|
||||
raise forms.ValidationError(
|
||||
_("Your username and/or password were incorrect.")
|
||||
|
@ -26,6 +26,6 @@ class LoginForm(forms.Form):
|
|||
# )
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
return email
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data.get('username')
|
||||
return username
|
||||
|
|
|
@ -65,6 +65,7 @@ INSTALLED_APPS = [
|
|||
'django.contrib.staticfiles',
|
||||
'bootstrap3',
|
||||
'dal',
|
||||
'rest_framework'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -208,3 +209,10 @@ if config('ENABLE_DEBUG_LOG', default=False, cast=bool):
|
|||
set_library_log_detail_level, OFF, BASIC, NETWORK, EXTENDED
|
||||
)
|
||||
set_library_log_detail_level(BASIC)
|
||||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
)
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@
|
|||
<button type="submit" class="btn choice-btn btn-block">
|
||||
{% trans "Change Password" %}
|
||||
</button>
|
||||
<hr>
|
||||
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -34,7 +34,13 @@
|
|||
{% trans "Change User Data" %}
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{% extends "base_short.html" %}
|
||||
{% load i18n staticfiles bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
<title> Verify your email. </title>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-bg"></div>
|
||||
<div class="auth-center">
|
||||
<div class="auth-content">
|
||||
<div class="auth-box">
|
||||
<h1 class="section-heading allcaps">{% trans " Check your email " %}</h1>
|
||||
<p class="text-center">{% trans "In order to complete the sign up process, please check your email and follow the activation instructions." %}</p>
|
||||
<form action="{% url 'index' %}" method="get" class="form" novalidated>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn choice-btn btn-block">
|
||||
{% trans "Back to indexpage" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -27,6 +27,7 @@
|
|||
</div>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
|
||||
<button type="submit" class="btn choicered-btn btn-block">
|
||||
{% trans "Delete Account" %}
|
||||
</button>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
{% extends "base_short.html" %}
|
||||
{% load i18n staticfiles bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
<title>Options for {{user}}</title>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-bg"></div>
|
||||
<div class="auth-center">
|
||||
<div class="auth-content">
|
||||
<div class="auth-box">
|
||||
<h1 class="section-heading allcaps">{% trans "Seeds of," %} {{user}}</h1><br><br>
|
||||
<table class="table table-hover text-center">
|
||||
<tbody>
|
||||
{% for i in seed %}
|
||||
<tr>
|
||||
<td>{{ i.realm }}</td>
|
||||
<td>{{ i.seed }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
<hr>
|
||||
<form class="form" novalidate>
|
||||
<div class="text-center">
|
||||
<a class="btn choice-btn btn-block" href="{% url 'index' %}" role="button">Back to Index</a><br>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -16,6 +16,7 @@
|
|||
<form class="form">
|
||||
<a class="btn choice-btn btn-block" href="{% url 'change_data' %}" role="button">{% trans "Change your userdata" %}</a><br>
|
||||
<a class="btn choice-btn btn-block" href="{% url 'change_password' %}" role="button">Change your password</a><br>
|
||||
<a class="btn choice-btn btn-block" href="{% url 'user_seeds' %}" role="button">Show seeds</a><br>
|
||||
<a class="btn choicered-btn btn-block" href="{% url 'logout' %}" role="button">{% trans "Logout" %}</a><br><br>
|
||||
</form>
|
||||
<br>
|
||||
|
|
|
@ -91,7 +91,7 @@ class LdapManager:
|
|||
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
|
||||
self._set_max_uid(uidNumber)
|
||||
try:
|
||||
uid = user.encode("utf-8")
|
||||
uid = user # user.encode("utf-8")
|
||||
conn.add("uid={uid},{customer_dn}".format(
|
||||
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
|
||||
),
|
||||
|
|
|
@ -13,19 +13,25 @@ from .views import (
|
|||
Index,
|
||||
LogOut,
|
||||
ResetRequest,
|
||||
UserCreateAPI
|
||||
UserCreateAPI,
|
||||
ActivateAccount,
|
||||
Seeds,
|
||||
SeedRetrieveCreate
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('register/', Register.as_view(), name="register"),
|
||||
path('create/', UserCreateAPI.as_view(), name="create"),
|
||||
path('changedata/', ChangeData.as_view(), name="change_data"),
|
||||
path('seeds/', Seeds.as_view(), name="user_seeds"),
|
||||
path('resetpassword/', ResetPassword.as_view(), name="reset_password"),
|
||||
path('changepassword/', ChangePassword.as_view(), name="change_password"),
|
||||
path('deleteaccount/', DeleteAccount.as_view(), name="account_delete"),
|
||||
path('index/', Index.as_view(), name="index"),
|
||||
path('logout/', LogOut.as_view(), name="logout"),
|
||||
path('reset/<str:user>/<str:token>/', ResetRequest.as_view()),
|
||||
path('activate/<str:user>/<str:pwd>/<str:firstname>/<str:lastname>/<str:email>/<str:token>/', ActivateAccount.as_view()),
|
||||
path('reset/', ResetRequest.as_view(), name="reset"),
|
||||
path('otp/', SeedRetrieveCreate.as_view(), name="seed"),
|
||||
path('', Index.as_view(), name="login_index"),
|
||||
]
|
265
dal/views.py
265
dal/views.py
|
@ -14,9 +14,10 @@ from rest_framework.response import Response
|
|||
from .models import ResetToken
|
||||
from .forms import LoginForm
|
||||
from .ungleich_ldap import LdapManager
|
||||
from decouple import config, Csv
|
||||
from pyotp import TOTP
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Imports for the extra stuff not in django
|
||||
|
@ -26,20 +27,70 @@ from datetime import datetime
|
|||
|
||||
from random import choice, randint
|
||||
import string
|
||||
import requests
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
|
||||
admin_seed = config('ADMIN_SEED')
|
||||
admin_name = config('ADMIN_NAME')
|
||||
admin_realm = config('ADMIN_REALM')
|
||||
user_realm = config('USER_REALM')
|
||||
otp_url = config('OTPSERVER')
|
||||
|
||||
|
||||
def activate_account_link(base_url, user, pwd, firstname, lastname, email, epochutc):
|
||||
tokengen = PasswordResetTokenGenerator()
|
||||
pseudouser = PseudoUser()
|
||||
token = tokengen.make_token(pseudouser)
|
||||
buser = bytes(user, 'utf-8')
|
||||
bpwd = bytes(pwd, 'utf-8')
|
||||
bfirstname = bytes(firstname, 'utf-8')
|
||||
blasttname = bytes(lastname, 'utf-8')
|
||||
bemail = bytes(email, 'utf-8')
|
||||
userpart = b64encode(buser)
|
||||
pwdpart = b64encode(bpwd)
|
||||
fnpart = b64encode(bfirstname)
|
||||
lnpart = b64encode(blasttname)
|
||||
mailpart = b64encode(bemail)
|
||||
# create entry into the database
|
||||
newdbentry = ResetToken(user=user, token=token, creation=epochutc)
|
||||
newdbentry.save()
|
||||
# set up the link
|
||||
link = "{base_url}/activate/{user}/{pwd}/{fn}/{ln}/{mail}/{token}/".format(
|
||||
base_url=base_url, user=userpart.decode('utf-8'),
|
||||
pwd=pwdpart.decode('utf-8'),
|
||||
fn=fnpart.decode('utf-8'),
|
||||
ln=lnpart.decode('utf-8'),
|
||||
mail=mailpart.decode('utf-8'),
|
||||
token=token
|
||||
)
|
||||
return link
|
||||
|
||||
|
||||
def clean_db():
|
||||
"""Revoves outdated tokens"""
|
||||
# cutoff time is set to 24h hours
|
||||
# using utcnow() to have no headache with timezones
|
||||
cutoff = int(datetime.utcnow().timestamp()) - (24*60*60)
|
||||
# Get all tokens older than 24 hours
|
||||
oldtokens = ResetToken.objects.all().filter(creation__lt=cutoff)
|
||||
for token in oldtokens:
|
||||
# delete all tokens older than 24 hours
|
||||
token.delete()
|
||||
return True
|
||||
|
||||
class Index(FormView):
|
||||
template_name = "landing.html"
|
||||
form_class = LoginForm
|
||||
success_url = 'useroptions.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
email = form.cleaned_data.get('email')
|
||||
username = form.cleaned_data.get('username')
|
||||
password = form.cleaned_data.get('password')
|
||||
user = authenticate(username=email, password=password)
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
login(self.request, user)
|
||||
return render(self.request, 'useroptions.html', { 'user': user } )
|
||||
|
@ -92,34 +143,30 @@ class Register(View):
|
|||
pwd = r'%s' % password1
|
||||
|
||||
try:
|
||||
ldap_manager = LdapManager()
|
||||
ldap_manager.create_user(
|
||||
username, pwd, firstname, lastname, email
|
||||
creationtime = int(datetime.utcnow().timestamp())
|
||||
base_url = "{0}://{1}".format(self.request.scheme,
|
||||
self.request.get_host())
|
||||
link = activate_account_link(base_url, username, pwd, firstname, lastname, email, creationtime)
|
||||
email_from = settings.EMAIL_FROM_ADDRESS
|
||||
to = ['%s <%s>' % (username, email)]
|
||||
subject = 'Activate your ungleich account'.format(firstname)
|
||||
body = 'You can activate your ungleich account account by clicking <a href="{link}">here</a>.' \
|
||||
' You can also copy and paste the following link into the address bar of your browser and follow' \
|
||||
' the link in order to activate your account.\n\n{link}'.format(link=link)
|
||||
# Build the email
|
||||
mail = EmailMessage(
|
||||
subject=subject,
|
||||
body=body,
|
||||
from_email=email_from,
|
||||
to=to
|
||||
)
|
||||
mail.send()
|
||||
|
||||
except Exception as e:
|
||||
return render(request, 'error.html', { 'urlname': urlname,
|
||||
'service': service,
|
||||
'error': e } )
|
||||
# Finally, we send the send user credentials via email
|
||||
creationtime = int(datetime.utcnow().timestamp())
|
||||
# Construct the data for the email
|
||||
email_from = settings.EMAIL_FROM_ADDRESS
|
||||
to = ['%s <%s>' % (username, email)]
|
||||
subject = '{}, Welcome to datacenterlight'.format(firstname)
|
||||
body = 'The username {} was successfully created.\n'.format(username)
|
||||
# Build the email
|
||||
mail = EmailMessage(
|
||||
subject=subject,
|
||||
body=body,
|
||||
from_email=email_from,
|
||||
to=to
|
||||
)
|
||||
try:
|
||||
mail.send()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
return render(request, 'usercreated.html', { 'user': username } )
|
||||
return render(request, 'confirm_email.html')
|
||||
|
||||
class ChangeData(LoginRequiredMixin, View):
|
||||
login_url = reverse_lazy('login_index')
|
||||
|
@ -297,7 +344,7 @@ class ResetRequest(View):
|
|||
# Cleans up outdated tokens
|
||||
# If we expect quite a bit of old tokens, maybe somewhere else is better,
|
||||
# but for now we don't really expect many unused tokens
|
||||
self.clean_db()
|
||||
clean_db()
|
||||
# If user and token are not supplied by django, it was called from somewhere else, so it's
|
||||
# invalid
|
||||
if user == None or token == None:
|
||||
|
@ -336,10 +383,10 @@ class ResetRequest(View):
|
|||
if password1 == "" or not password1 or password2 == "" or not password2:
|
||||
return render(request, 'error.html', { 'service': service, 'error': 'Please supply a password and confirm it.' } )
|
||||
if password1 != password2:
|
||||
return render(request, 'error.html', { 'service': service, 'error': 'The supplied passwords do not match.' } )
|
||||
return render(request, 'error.html', {'service': service, 'error': 'The supplied passwords do not match.'})
|
||||
if len(password1) < 8:
|
||||
return render(request, 'error.html', { 'service': service, 'error': 'The password is too short, please use a longer one. At least 8 characters.' } )
|
||||
# everything checks out, now change the password
|
||||
return render(request, 'error.html', {'service': service,
|
||||
'error': 'The password is too short, please use a longer one. At least 8 characters.'})
|
||||
|
||||
ldap_manager = LdapManager()
|
||||
result = ldap_manager.change_password(
|
||||
|
@ -353,17 +400,7 @@ class ResetRequest(View):
|
|||
else:
|
||||
return render(request, 'error.html', { 'service': service, 'error': result } )
|
||||
|
||||
# Cleans up outdated tokens
|
||||
def clean_db(self):
|
||||
# cutoff time is set to 24h hours
|
||||
# using utcnow() to have no headache with timezones
|
||||
cutoff = int(datetime.utcnow().timestamp()) - (24*60*60)
|
||||
# Get all tokens older than 24 hours
|
||||
oldtokens = ResetToken.objects.all().filter(creation__lt=cutoff)
|
||||
for token in oldtokens:
|
||||
# delete all tokens older than 24 hours
|
||||
token.delete()
|
||||
return True
|
||||
|
||||
|
||||
# The logged in user can change the password here
|
||||
|
||||
|
@ -488,6 +525,58 @@ class PseudoUser():
|
|||
pk = ''.join(choice(string.ascii_letters + string.digits) for _ in range(20))
|
||||
password = ''.join(choice(string.ascii_letters + string.digits) for _ in range(30))
|
||||
|
||||
|
||||
class ActivateAccount(View):
|
||||
|
||||
def get(self, request, user=None, pwd=None, firstname=None, lastname=None, email=None, token=None):
|
||||
clean_db()
|
||||
if token is None:
|
||||
return HttpResponse('Invalid URL', status=404)
|
||||
elem_list = [user, pwd, firstname, lastname, email]
|
||||
clean_list = []
|
||||
for value in elem_list:
|
||||
try:
|
||||
value_temp = bytes(value, 'utf-8')
|
||||
value_decode = b64decode(value_temp)
|
||||
value_clean = value_decode.decode('utf-8')
|
||||
clean_list.append(value_clean)
|
||||
except Exception as e:
|
||||
return HttpResponse('Invalid URL', status=404)
|
||||
checks_out = False
|
||||
dbentries = ResetToken.objects.all().filter(user=clean_list[0])
|
||||
for entry in dbentries:
|
||||
if entry.token == token:
|
||||
# found the token, now delete it since it's used
|
||||
checks_out = True
|
||||
entry.delete()
|
||||
# No token was found
|
||||
if not checks_out:
|
||||
return HttpResponse('Invalid URL.', status=404)
|
||||
# Token was found, create user
|
||||
try:
|
||||
ldap_manager = LdapManager()
|
||||
ldap_manager.create_user(
|
||||
clean_list[0], clean_list[1], clean_list[2], clean_list[3], clean_list[4]
|
||||
)
|
||||
req = requests.post(otp_url, data=json.dumps(
|
||||
{
|
||||
'auth_token': TOTP(admin_seed).now(),
|
||||
'auth_name': admin_name,
|
||||
'auth_realm': admin_realm,
|
||||
'name': clean_list[0],
|
||||
'realm': user_realm
|
||||
}), headers={'Content-Type': 'application/json'})
|
||||
if req.status_code != 201:
|
||||
logger.error("User {} failed to create its otp seed".format(clean_list[0]))
|
||||
|
||||
#Send welcome email
|
||||
except Exception as e:
|
||||
return render(request, 'error.html', {'urlname': 'register',
|
||||
'service': 'register an user',
|
||||
'error': e})
|
||||
return render(request, 'usercreated.html', { 'user': clean_list[0] } )
|
||||
|
||||
|
||||
class UserCreateAPI(APIView):
|
||||
|
||||
def post(self, request):
|
||||
|
@ -508,25 +597,24 @@ class UserCreateAPI(APIView):
|
|||
|
||||
pwd = r'%s' % User.objects.make_random_password()
|
||||
|
||||
try:
|
||||
ldap_manager = LdapManager()
|
||||
ldap_manager.create_user(
|
||||
username, pwd, firstname, lastname, email
|
||||
)
|
||||
except Exception as e:
|
||||
return Response('While trying to create the user, an error was encountered: %s' % e, 400)
|
||||
|
||||
# send user credentials via email
|
||||
base_url = "{0}://{1}".format(self.request.scheme,
|
||||
self.request.get_host())
|
||||
creationtime = int(datetime.utcnow().timestamp())
|
||||
link = activate_account_link(base_url, username, pwd, firstname, lastname, email, creationtime)
|
||||
|
||||
# Construct the data for the email
|
||||
email_from = settings.EMAIL_FROM_ADDRESS
|
||||
to = ['%s <%s>' % (username, email)]
|
||||
subject = 'Your datacenterlight credentials'
|
||||
body = 'Your user was successfully created.\n'
|
||||
subject = 'Ungleich account creation.'
|
||||
body = 'A request has been sent to our servers to register you as a ungleich user.\n'
|
||||
body += 'In order to complete the registration process you must ' \
|
||||
'click <a href="{link}">here</a> or copy & paste the following link into the address bar of ' \
|
||||
'your browser.\n{link}\n'.format(link=link)
|
||||
body += 'Your credentials are:\n'
|
||||
body += 'Username: %s\n\n' % username
|
||||
body += 'Password: %s\n\n' % pwd
|
||||
body += 'We strongly recommend you to after log in change your password.\n'
|
||||
body += 'We strongly recommend after the activation to log in and change your password.\n'
|
||||
body += 'This link will remain active for 24 hours.\n'
|
||||
# Build the email
|
||||
mail = EmailMessage(
|
||||
subject=subject,
|
||||
|
@ -536,6 +624,71 @@ class UserCreateAPI(APIView):
|
|||
)
|
||||
try:
|
||||
mail.send()
|
||||
except:
|
||||
return Response('User was created, but failed to send the email', 201)
|
||||
return Response('User successfully created', 200)
|
||||
except Exception as e:
|
||||
return Response('Failed to send the email, please try again', 400)
|
||||
return Response('An email with activation link has been sent in order to complete your registration. Please check your inbox.', 200)
|
||||
|
||||
|
||||
class SeedRetrieveCreate(APIView):
|
||||
def post(self, request):
|
||||
try:
|
||||
username = request.data['username']
|
||||
password = request.data[r'password']
|
||||
realm = request.data['realm']
|
||||
print(password)
|
||||
except KeyError:
|
||||
return Response('You need to specify username, password, and realm values', 400)
|
||||
# authenticate the user against ldap
|
||||
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
|
||||
req = requests.get(otp_url, data=json.dumps(
|
||||
{
|
||||
'auth_token': TOTP(admin_seed).now(),
|
||||
'auth_name': admin_name,
|
||||
'auth_realm': admin_realm}), headers={'Content-Type': 'application/json'})
|
||||
|
||||
response_data = json.loads(req.text)
|
||||
|
||||
for elem in response_data:
|
||||
if elem['name'] == username and elem['realm'] == realm:
|
||||
return Response(elem, 200)
|
||||
# If doesn't find a match then check if the realm is allowed and create the user
|
||||
allowed_realms = config('ALLOWED_REALMS', cast=Csv())
|
||||
if realm not in allowed_realms:
|
||||
return Response('Not allowed to perform this action.', 403)
|
||||
else:
|
||||
req = requests.post(otp_url, data=json.dumps(
|
||||
{
|
||||
'auth_token': TOTP(admin_seed).now(),
|
||||
'auth_name': admin_name,
|
||||
'auth_realm': admin_realm,
|
||||
'name': username,
|
||||
'realm': realm
|
||||
}), headers={'Content-Type': 'application/json'})
|
||||
if req.status_code == 201:
|
||||
msg = json.loads(req.text)
|
||||
return Response(msg, 201)
|
||||
else:
|
||||
return Response(json.loads(req.text), req.status_code)
|
||||
|
||||
else:
|
||||
return Response('Invalid Credentials', 400)
|
||||
|
||||
|
||||
class Seeds(LoginRequiredMixin, View):
|
||||
login_url = reverse_lazy('login_index')
|
||||
def get(self, request):
|
||||
seedlist = []
|
||||
response = requests.get(
|
||||
otp_url,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps(
|
||||
{'auth_name': admin_name, 'auth_realm': admin_realm, 'auth_token': TOTP(admin_seed).now()}))
|
||||
response_data = json.loads(response.text)
|
||||
for i in range(len(response_data)):
|
||||
if response_data[i]['name'] == request.user.username:
|
||||
value = {'realm': response_data[i]['realm'], 'seed': response_data[i]['seed']}
|
||||
seedlist.append(value)
|
||||
return render(request, 'seed_list.html', {'seed': seedlist})
|
||||
|
|
|
@ -5,4 +5,6 @@ django-bootstrap3
|
|||
django-filter==2.1.0
|
||||
python-decouple
|
||||
ldap3
|
||||
djangorestframework
|
||||
djangorestframework
|
||||
pyotp
|
||||
requests
|
Loading…
Reference in New Issue