diff --git a/matrixhosting/static/matrixhosting/css/theme.css b/matrixhosting/static/matrixhosting/css/theme.css index 416c4d2..1eec6ba 100644 --- a/matrixhosting/static/matrixhosting/css/theme.css +++ b/matrixhosting/static/matrixhosting/css/theme.css @@ -3489,4 +3489,20 @@ body, html { } #card-errors { color: #e41d25; - } \ No newline at end of file + } + + .bill-cancelled { + background-color: #e41d25 !important; + } + .bill-paid { + background-color: #28a745!important; + } + .bill-new { + background-color: #17a2b8!important; + } + .email_list .unverified { + color: #f7656e; + } + .email_list .primary { + color: #126567; + } diff --git a/matrixhosting/static/matrixhosting/js/payment.js b/matrixhosting/static/matrixhosting/js/payment.js index 3494d5e..79b67f1 100644 --- a/matrixhosting/static/matrixhosting/js/payment.js +++ b/matrixhosting/static/matrixhosting/js/payment.js @@ -3,10 +3,15 @@ function fetch_pricing() { var cores = $('#cores').val(); var memory = $('#memory').val(); var storage = $('#storage').val(); + var country = $('select[name="country"]').val(); + var data = { cores: cores, memory: memory, storage: storage}; + if (country != undefined) { + data['country'] = country; + } $.ajax({ type: 'GET', url: url, - data: { cores: cores, memory: memory, storage: storage}, + data: data, dataType: 'json', success: function (data) { if (data && data['total']) { @@ -27,6 +32,20 @@ function fetch_pricing() { }); }; +function init_checkout_btn() { + var selected_opt = $('input[name="payment_card"]:checked').val(); + if( selected_opt == 'new') { + $('#checkout-btn').hide(); + $('#newcard').show(); + } else if(selected_opt == undefined) { + $('#newcard').hide(); + $('#checkout-btn').hide(); + } else { + $('#newcard').hide(); + $('#checkout-btn').show(); + } +} + function incrementValue(e) { var valueElement = $(e.target).parent().parent().find('input'); var step = $(valueElement).attr('step'); @@ -147,15 +166,13 @@ $(document).ready(function () { submitBillingForm(); }); - + init_checkout_btn(); + $('input[name="payment_card"]').change(function(e) { - if($('input[name="payment_card"]:checked').val() == 'new') { - $('#checkout-btn').hide(); - $('#newcard').show(); - } else { - $('#newcard').hide(); - $('#checkout-btn').show(); - } + init_checkout_btn(); + }); + $('select[name="country"]').change(function(e) { + fetch_pricing(); }); }); diff --git a/matrixhosting/tasks.py b/matrixhosting/tasks.py index c681e8c..69d1b40 100644 --- a/matrixhosting/tasks.py +++ b/matrixhosting/tasks.py @@ -22,7 +22,7 @@ def send_warning_email(bill, html_message): next_run=timezone.now() + timedelta(hours=1)) def charge_open_bills(): - un_paid_bills = Bill.objects.filter(is_closed=False) + un_paid_bills = Bill.objects.filter(status="new") for bill in un_paid_bills: date_diff = (date.today() - bill.due_date.date()).days # If there is not enough money in the account 7 days before renewal, the system sends a warning @@ -47,8 +47,8 @@ def charge_open_bills(): if balance < 0: payment = Payment.objects.create(owner=bill.owner, amount=balance, source='stripe') if payment: - bill.close() - bill.close() + bill.close(status="paid") + bill.close(status="cancelled") except Exception as e: log.error(f"It seems that there is issue in payment for {bill.owner.name}", e) # do nothing diff --git a/matrixhosting/templates/account/base.html b/matrixhosting/templates/account/base.html index 9ee0385..207ad05 100644 --- a/matrixhosting/templates/account/base.html +++ b/matrixhosting/templates/account/base.html @@ -1,4 +1,4 @@ -{% load static i18n %} {% get_current_language as LANGUAGE_CODE %} +{% load static compress i18n %} {% get_current_language as LANGUAGE_CODE %} @@ -19,7 +19,9 @@ ============================================= --> - +{% compress css %} + +{% endcompress %} @@ -60,18 +62,31 @@
+ {% if messages %} +
+ {% for message in messages %} + {% if 'error' in message.tags %} + + {% else %} + + {% endif %} + {% endfor %} +
+ {% endif %} {% block content %} {% endblock %} - {% if messages %} -
- Messages: -
    - {% for message in messages %} -
  • {{message}}
  • - {% endfor %} -
-
- {% endif %}
diff --git a/matrixhosting/templates/account/email.html b/matrixhosting/templates/account/email.html new file mode 100644 index 0000000..4efb7cc --- /dev/null +++ b/matrixhosting/templates/account/email.html @@ -0,0 +1,74 @@ +{% extends "account/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %} + +{% block content %} +

{% trans "E-mail Addresses" %}

+{% if user.emailaddress_set.all %} +

{% trans 'The following e-mail addresses are associated with your account:' %}

+ +
+{% csrf_token %} +
+ + {% for emailaddress in user.emailaddress_set.all %} +
+ +
+ {% endfor %} + +
+ + + +
+ +
+
+ +{% else %} +

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

+ +{% endif %} + + + +{% endblock %} + + +{% block extra_body %} + +{% endblock %} \ No newline at end of file diff --git a/matrixhosting/templates/account/email_confirm.html b/matrixhosting/templates/account/email_confirm.html index ac0891b..0ba7d45 100644 --- a/matrixhosting/templates/account/email_confirm.html +++ b/matrixhosting/templates/account/email_confirm.html @@ -5,7 +5,6 @@ {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} - {% block content %}

{% trans "Confirm E-mail Address" %}

@@ -17,7 +16,7 @@
{% csrf_token %} - +
{% else %} diff --git a/matrixhosting/templates/account/password_reset_from_key.html b/matrixhosting/templates/account/password_reset_from_key.html index 16f27e9..b98d174 100644 --- a/matrixhosting/templates/account/password_reset_from_key.html +++ b/matrixhosting/templates/account/password_reset_from_key.html @@ -13,8 +13,22 @@ {% if form %}
{% csrf_token %} - {{ form.as_p }} - + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} +
+ + + {{ form.login.errors }} +
+
+ + + {{ form.login.errors }} +
+
{% else %}

{% trans 'Your password is now changed.' %}

diff --git a/matrixhosting/templates/account/signup.html b/matrixhosting/templates/account/signup.html index dab2230..ea1d625 100644 --- a/matrixhosting/templates/account/signup.html +++ b/matrixhosting/templates/account/signup.html @@ -3,10 +3,10 @@ {% load i18n %} {% load account socialaccount %} -{% block head_title %}{% trans "Sign In" %}{% endblock %} +{% block head_title %}{% trans "Sign Up" %}{% endblock %} {% block content %} -

Log In

+

{% trans "Sign Up" %}

{% csrf_token %} {% if form.non_field_errors %} @@ -19,8 +19,22 @@ {{ form.username.errors }} +
+
+
+ + +
+
+
+
+ + +
+
+
- + {{ form.email.errors }}
diff --git a/matrixhosting/templates/matrixhosting/bills.html b/matrixhosting/templates/matrixhosting/bills.html index c85b237..46fd56d 100644 --- a/matrixhosting/templates/matrixhosting/bills.html +++ b/matrixhosting/templates/matrixhosting/bills.html @@ -9,7 +9,7 @@
@@ -55,8 +55,8 @@
{% trans "Creation Date"%}
{% trans "Amount"%}
{% trans "Due Date"%}
-
{% trans "Closed"%}
-
{% trans "Download"%}
+
{% trans "Status"%}
+
@@ -74,14 +74,10 @@
{{bill.creation_date|date:"Y-m-d"}}
{{bill.sum}} {{bill.currency}}
{{bill.due_date|date:"Y-m-d"}}
-
- {% if bill.is_closed %} - - {%else%} - - {%endif%} +
+ {{bill.get_status_display}}
-
+
diff --git a/matrixhosting/templates/matrixhosting/cards.html b/matrixhosting/templates/matrixhosting/cards.html index 5e7ada3..0f035eb 100644 --- a/matrixhosting/templates/matrixhosting/cards.html +++ b/matrixhosting/templates/matrixhosting/cards.html @@ -9,7 +9,7 @@
@@ -179,7 +179,9 @@ $.ajax({ type: 'DELETE', url: url, - data: {csrfmiddlewaretoken: '{{ csrf_token }}'}, + beforeSend: function(xhr) { + xhr.setRequestHeader("X-CSRFToken", '{{ csrf_token }}'); + }, dataType: 'json', success: function (result) { location.reload(); diff --git a/matrixhosting/templates/matrixhosting/includes/_calculator_form.html b/matrixhosting/templates/matrixhosting/includes/_calculator_form.html index 7f34a7b..927020a 100644 --- a/matrixhosting/templates/matrixhosting/includes/_calculator_form.html +++ b/matrixhosting/templates/matrixhosting/includes/_calculator_form.html @@ -4,7 +4,7 @@

- {% if matrix_vm_pricing.set_up_fees %}Setup Fees{{ matrix_vm_pricing.set_up_fees }} CHF
{% endif %} + {% if matrix_vm_pricing.set_up_fees %}Setup Fees{{ matrix_vm_pricing.set_up_fees }} CHF included
{% endif %} {% if matrix_vm_pricing.discount_amount %} {% trans "Discount" %} {{ matrix_vm_pricing.discount_amount }} CHF {% endif %} @@ -54,8 +54,8 @@ {% if matrix_vm_pricing.discount_amount %}

{% trans "You save" %} {{ matrix_vm_pricing.discount_amount }} CHF

{% endif %} -

{% trans "Subtotal" %} CHF/{% trans "month" %}

-

{% trans "Total" %}CHF/{% trans "month" %}

+

{% trans "Subtotal" %} CHF

+

{% trans "Total" %}CHF

diff --git a/matrixhosting/templates/matrixhosting/order_details.html b/matrixhosting/templates/matrixhosting/order_details.html index 0f3bdfe..07ce22a 100644 --- a/matrixhosting/templates/matrixhosting/order_details.html +++ b/matrixhosting/templates/matrixhosting/order_details.html @@ -186,9 +186,9 @@ {% with cards_len=cards|length %}

{% if cards_len > 0 %} - {% blocktrans %}You haven't enough balance in your wallet, Please select one of the cards that you used before or fill in your credit card information below.{% endblocktrans %} + {% blocktrans %}There is not enough balance in your account to proceed with this order. You can select a card or add a new card to fill up your account balance to proceed with the order.{% endblocktrans %} {% else %} - {% blocktrans %}You haven't enough balance in your wallet, Please fill in your credit card information below.{% endblocktrans %} + {% blocktrans %}There is not enough balance in your account to proceed with this order. Please fill in your credit card information below.{% endblocktrans %} {% endif %}

@@ -229,7 +229,7 @@

- {% blocktrans %}Your wallet has enough balance, Press Continue to fill the VM instance settings.{% endblocktrans %} + {% blocktrans %}You can use your account balance to make the payment. Press Continue to select the domain settings. You can review and confirm your order and payment in the next page.{% endblocktrans %}

diff --git a/matrixhosting/templates/matrixhosting/payments.html b/matrixhosting/templates/matrixhosting/payments.html index 780cb09..d1b62bf 100644 --- a/matrixhosting/templates/matrixhosting/payments.html +++ b/matrixhosting/templates/matrixhosting/payments.html @@ -9,7 +9,7 @@
diff --git a/matrixhosting/utils.py b/matrixhosting/utils.py index ed0a7f0..0df0129 100644 --- a/matrixhosting/utils.py +++ b/matrixhosting/utils.py @@ -24,6 +24,6 @@ def finalize_order(request, customer, billing_address, if payment: #Close the bill as the payment has been added VMInstance.create_instance(order) - bill.close() + bill.close(status="paid") return order, bill \ No newline at end of file diff --git a/matrixhosting/views.py b/matrixhosting/views.py index 197cf10..afd50d6 100644 --- a/matrixhosting/views.py +++ b/matrixhosting/views.py @@ -30,6 +30,7 @@ import uncloud_pay.stripe as uncloud_stripe from .models import VMInstance from .serializers import * from .utils import * +from ldap.ldapobject import LDAPObject logger = logging.getLogger(__name__) diff --git a/uncloud/.env b/uncloud/.env index 964945c..f3ecfdd 100644 --- a/uncloud/.env +++ b/uncloud/.env @@ -1,6 +1,6 @@ ALLOWED_HOSTS= STRIPE_KEY= -STRIPE_PUBLIC_KEY=p +STRIPE_PUBLIC_KEY= DATABASE_ENGINE=django.db.backends.sqlite3 DATABASE_NAME= DATABASE_HOST= @@ -16,4 +16,14 @@ GITLAB_PROJECT_ID= GITLAB_OAUTH_TOKEN= GITLAB_AUTHOR_EMAIL= GITLAB_AUTHOR_NAME= -WKHTMLTOPDF_CMD=/usr/local/bin/wkhtmltopdf \ No newline at end of file +WKHTMLTOPDF_CMD= +LDAP_DEFAULT_START_UID= +AUTH_LDAP_SERVER_HOST= +AUTH_LDAP_SERVER_URI= +AUTH_LDAP_BIND_DN= +AUTH_LDAP_BIND_PASSWORD= +LDAP_ADMIN_DN= +LDAP_ADMIN_PASSWORD= +LDAP_CUSTOMER_GROUP_ID= +LDAP_CUSTOMER_DN= + diff --git a/uncloud/forms.py b/uncloud/forms.py index 153a49a..135b3c1 100644 --- a/uncloud/forms.py +++ b/uncloud/forms.py @@ -1,8 +1,8 @@ from django import forms from django.contrib.auth.models import User - class UserDeleteForm(forms.ModelForm): class Meta: model = User fields = [] + diff --git a/uncloud/settings.py b/uncloud/settings.py index 53c8478..0548abf 100644 --- a/uncloud/settings.py +++ b/uncloud/settings.py @@ -19,8 +19,19 @@ import environ from django.core.management.utils import get_random_secret_key from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion - -LOGGING = {} +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, +} # Initialise environment variables env = environ.Env() @@ -135,25 +146,40 @@ AUTH_PASSWORD_VALIDATORS = [ # Authall Settings ACCOUNT_AUTHENTICATION_METHOD = "username" ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 -ACCOUNT_EMAIL_REQUIRED = False -ACCOUNT_EMAIL_VERIFICATION = "optional" -ACCOUNT_UNIQUE_EMAIL = False +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_UNIQUE_EMAIL = True +MAX_EMAIL_ADDRESSES = 1 ################################################################################ # AUTH/LDAP -AUTH_LDAP_SERVER_URI = "" -AUTH_LDAP_BIND_DN = "" -AUTH_LDAP_BIND_PASSWORD = "" -AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", +LDAP_ENABLED = True +AUTH_LDAP_SERVER_HOST = env('AUTH_LDAP_SERVER_HOST') +AUTH_LDAP_SERVER_URI = env('AUTH_LDAP_SERVER_URI') +AUTH_LDAP_BIND_DN = env('AUTH_LDAP_BIND_DN') +AUTH_LDAP_BIND_PASSWORD = env('AUTH_LDAP_BIND_PASSWORD') + +AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=customers,dc=ungleich,dc=ch" +AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=customers,dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") +# BIND_AS_AUTHENTICATING_USER = True +START_TLS = True +LDAP_ADMIN_DN = env("LDAP_ADMIN_DN") +LDAP_ADMIN_PASSWORD = env("LDAP_ADMIN_PASSWORD") +LDAP_CUSTOMER_GROUP_ID = env("LDAP_CUSTOMER_GROUP_ID") +LDAP_CUSTOMER_DN=env("LDAP_CUSTOMER_DN") +#AUTH_LDAP_USER_QUERY_FIELD = "email" AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", + "first_name": "cn", "last_name": "sn", "email": "mail" } +LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID')) +LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH', + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file') +) ################################################################################ # AUTH/Django AUTHENTICATION_BACKENDS = [ @@ -162,9 +188,16 @@ AUTHENTICATION_BACKENDS = [ 'allauth.account.auth_backends.AuthenticationBackend', ] -AUTH_USER_MODEL = 'uncloud_auth.User' +AUTH_USER_MODEL = 'uncloud_auth.User' +ACCOUNT_FORMS = { + 'signup': 'uncloud_auth.forms.MySignupForm', + 'change_password': 'uncloud_auth.forms.MyChangePasswordForm', + 'set_password': 'uncloud_auth.forms.MySetPasswordForm', + 'reset_password_from_key': 'uncloud_auth.forms.MyResetPasswordKeyForm', + } + ################################################################################ # AUTH/REST REST_FRAMEWORK = { @@ -233,26 +266,14 @@ UNCLOUD_ADMIN_NAME = "uncloud-admin" LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' -# replace these in local_settings.py -AUTH_LDAP_SERVER_URI = "ldaps://ldap1.example.com,ldaps://ldap2.example.com" -AUTH_LDAP_BIND_DN="uid=django,ou=system,dc=example,dc=com" -AUTH_LDAP_BIND_PASSWORD="a very secure ldap password" -AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", - ldap.SCOPE_SUBTREE, - "(uid=%(user)s)") - -# where to create customers -LDAP_CUSTOMER_DN="ou=customer,dc=example,dc=com" - EMAIL_USE_TLS = True EMAIL_HOST = env('EMAIL_HOST') + EMAIL_PORT = 25 EMAIL_HOST_USER = DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') DEFAULT_FROM_EMAIL = 'support@ungleich.ch' RENEWAL_FROM_EMAIL = 'support@ungleich.ch' -# Should be removed in production -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ############## # Jobs diff --git a/uncloud_auth/forms.py b/uncloud_auth/forms.py new file mode 100644 index 0000000..11ec9c0 --- /dev/null +++ b/uncloud_auth/forms.py @@ -0,0 +1,69 @@ +import logging + +from allauth.account.forms import SignupForm, ChangePasswordForm, ResetPasswordKeyForm, SetPasswordForm +from django import forms as d_forms +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from .ungleich_ldap import LdapManager + +logger = logging.getLogger(__name__) + +class MySignupForm(SignupForm): + first_name = d_forms.CharField(max_length=30) + last_name = d_forms.CharField(max_length=30) + + def custom_signup(self, request, user): + user.first_name = self.cleaned_data["first_name"] + user.last_name = self.cleaned_data["last_name"] + user.save() + if settings.LDAP_ENABLED: + ldap_manager = LdapManager() + try: + user_exists_in_ldap, entries = ldap_manager.check_user_exists(user.username) + except Exception: + logger.exception("Exception occur while searching for user in LDAP") + else: + if not user_exists_in_ldap: + ldap_manager.create_user(user.username, user.password, user.first_name, user.last_name, user.email) + +class MyResetPasswordKeyForm(ResetPasswordKeyForm): + def save(self): + ldap_manager = LdapManager() + try: + user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username) + if not user_exists_in_ldap: + super(MyResetPasswordKeyForm, self).save() + else: + if ldap_manager.change_password(entries[0].entry_dn, self.cleaned_data["password1"]): + super(MyResetPasswordKeyForm, self).save() + except Exception: + logger.exception("Exception occur while searching for user in LDAP") + raise d_forms.ValidationError(_("An error occurred, please try again later")) + +class MyChangePasswordForm(ChangePasswordForm): + def save(self): + ldap_manager = LdapManager() + try: + user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username) + if not user_exists_in_ldap: + super(MyChangePasswordForm, self).save() + else: + if ldap_manager.change_password(self.user.username, self.cleaned_data["password1"]): + super(MyChangePasswordForm, self).save() + except Exception: + logger.exception("Exception occur while searching for user in LDAP") + raise d_forms.ValidationError(_("An error occurred, please try again later")) + +class MySetPasswordForm(SetPasswordForm): + def save(self): + ldap_manager = LdapManager() + try: + user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.user.username) + if not user_exists_in_ldap: + super(MySetPasswordForm, self).save() + else: + if ldap_manager.change_password(self.user.username, self.cleaned_data["password1"]): + super(MySetPasswordForm, self).save() + except Exception: + logger.exception("Exception occur while searching for user in LDAP") + raise d_forms.ValidationError(_("An error occurred, please try again later")) diff --git a/uncloud_auth/uldap.py b/uncloud_auth/uldap.py deleted file mode 100644 index aa90c77..0000000 --- a/uncloud_auth/uldap.py +++ /dev/null @@ -1,42 +0,0 @@ -import ldap -# from django.conf import settings - -AUTH_LDAP_SERVER_URI = "ldaps://ldap1.ungleich.ch,ldaps://ldap2.ungleich.ch" -AUTH_LDAP_BIND_DN="uid=django-create,ou=system,dc=ungleich,dc=ch" -AUTH_LDAP_BIND_PASSWORD="kS#e+v\zjKn]L!,RIu2}V+DUS" -# AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch", -# ldap.SCOPE_SUBTREE, -# "(uid=%(user)s)") - - - -ldap_object = ldap.initialize(AUTH_LDAP_SERVER_URI) -cancelid = ldap_object.bind(AUTH_LDAP_BIND_DN, AUTH_LDAP_BIND_PASSWORD) - -res = ldap_object.search_s("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=nico)") -print(res) - -# class LDAP(object): -# """ -# Managing users in LDAP - -# Requires the following settings? - -# LDAP_USER_DN: where to create users in the tree - -# LDAP_ADMIN_DN: which DN to use for managing users -# LDAP_ADMIN_PASSWORD: which password to used - -# This module will reuse information from djagno_auth_ldap, including: - -# AUTH_LDAP_SERVER_URI - -# """ -# def __init__(self): -# pass - -# def create_user(self): -# pass - -# def change_password(self): -# pass diff --git a/uncloud_auth/ungleich_ldap.py b/uncloud_auth/ungleich_ldap.py index f22b423..39d8bf5 100644 --- a/uncloud_auth/ungleich_ldap.py +++ b/uncloud_auth/ungleich_ldap.py @@ -4,7 +4,9 @@ import logging import random import ldap3 +from ldap3 import ALL from django.conf import settings +from django.contrib.auth.hashers import make_password logger = logging.getLogger(__name__) @@ -21,7 +23,7 @@ class LdapManager: Initialize the LDAP subsystem. """ self.rng = random.SystemRandom() - self.server = ldap3.Server(settings.AUTH_LDAP_SERVER) + self.server = ldap3.Server(settings.AUTH_LDAP_SERVER_HOST, use_ssl=True, get_info=ALL) def get_admin_conn(self): @@ -32,6 +34,7 @@ class LdapManager: conn = self.get_conn(user=settings.LDAP_ADMIN_DN, password=settings.LDAP_ADMIN_PASSWORD, raise_exceptions=True) + conn.start_tls() conn.bind() return conn @@ -58,7 +61,6 @@ class LdapManager: base64 or decoding it to unicode from ``ascii``. """ SALT_BYTES = 15 - sha1 = hashlib.sha1() salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little") @@ -67,10 +69,11 @@ class LdapManager: digest = sha1.digest() passwd = b"{SSHA}" + base64.b64encode(digest + salt) + return passwd - def create_user(self, user, password, firstname, lastname, email): + def create_user(self, username, password, firstname, lastname, email): conn = self.get_admin_conn() uidNumber = self._get_max_uid() + 1 logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber)) @@ -91,7 +94,7 @@ class LdapManager: logger.debug("{uid} does not exist. Using it".format(uid=uidNumber)) self._set_max_uid(uidNumber) try: - uid = user # user.encode("utf-8") + uid = username conn.add("uid={uid},{customer_dn}".format( uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN ), @@ -105,54 +108,43 @@ class LdapManager: "uidNumber": [str(uidNumber)], "gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)], "loginShell": ["/bin/bash"], - "homeDirectory": ["/home/{}".format(user).encode("utf-8")], + "homeDirectory": ["/home/{}".format(username).encode("utf-8")], "mail": email.encode("utf-8"), - "userPassword": [self._ssha_password( - password.encode("utf-8") - )] + "userPassword": [self._ssha_password(password.encode("utf-8"))] } ) - logger.debug('Created user %s %s' % (user.encode('utf-8'), + logger.debug('Created user %s %s' % (username.encode('utf-8'), uidNumber)) except Exception as ex: - logger.debug('Could not create user %s' % user.encode('utf-8')) + logger.debug('Could not create user %s' % username.encode('utf-8')) logger.error("Exception: " + str(ex)) raise finally: conn.unbind() - def change_password(self, uid, new_password): + def change_password(self, entry_dn, new_password): """ Changes the password of the user identified by user_dn - :param uid: str The uid that identifies the user + :param entry_dn: str The dn that identifies the user :param new_password: str The new password string :return: True if password was changed successfully False otherwise """ conn = self.get_admin_conn() - - # Make sure the user exists first to change his/her details - user_exists, entries = self.check_user_exists( - uid=uid, - search_base=settings.ENTIRE_SEARCH_BASE - ) return_val = False - if user_exists: - try: - return_val = conn.modify( - entries[0].entry_dn, - { - "userpassword": ( - ldap3.MODIFY_REPLACE, - [self._ssha_password(new_password.encode("utf-8"))] - ) - } - ) - except Exception as ex: - logger.error("Exception: " + str(ex)) - else: - logger.error("User {} not found".format(uid)) + try: + return_val = conn.modify( + entry_dn, + { + "userpassword": ( + ldap3.MODIFY_REPLACE, + [self._ssha_password(new_password.encode("utf-8"))] + ) + } + ) + except Exception as ex: + logger.error("Exception: " + str(ex)) conn.unbind() return return_val @@ -173,7 +165,7 @@ class LdapManager: # Make sure the user exists first to change his/her details user_exists, entries = self.check_user_exists( uid=uid, - search_base=settings.ENTIRE_SEARCH_BASE + search_base=settings.LDAP_CUSTOMER_DN ) return_val = False diff --git a/uncloud_pay/migrations/0031_auto_20210819_1304.py b/uncloud_pay/migrations/0031_auto_20210819_1304.py new file mode 100644 index 0000000..a3d75db --- /dev/null +++ b/uncloud_pay/migrations/0031_auto_20210819_1304.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.4 on 2021-08-19 13:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0030_pricingplan_monthly_maintenance_fees'), + ] + + operations = [ + migrations.RemoveField( + model_name='bill', + name='is_closed', + ), + migrations.AddField( + model_name='bill', + name='status', + field=models.CharField(choices=[('new', 'New'), ('cancelled', 'Cancelled'), ('paid', 'Paid')], default='new', max_length=32), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 20bed38..4e9949b 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -27,6 +27,12 @@ from .services import * # Used to generate bill due dates. BILL_PAYMENT_DELAY=datetime.timedelta(days=settings.BILL_PAYMENT_DELAY) +EU_COUNTRIES = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk', + 'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it', + 'lv', 'lu', 'mt', 'nl', 'po', 'pt', 'ro','sk', 'si', 'es', + 'se', 'gb'] + + # Initialize logger. logger = logging.getLogger(__name__) @@ -254,41 +260,26 @@ class VATRate(models.Model): description = models.TextField(blank=True, default='') @staticmethod - def get_for_country(country_code): - vat_rate = None - try: - vat_rate = VATRate.objects.get( - territory_codes=country_code, start_date__isnull=False, stop_date=None - ) - return vat_rate.rate - except VATRate.DoesNotExist as dne: - logger.debug(str(dne)) - logger.debug("Did not find VAT rate for %s, returning 0" % country_code) - return 0 - - @staticmethod - def get_vat_rate(billing_address, when=None): + def get_vat_rate_for_country(country, when=None): """ Returns the VAT rate for business to customer. B2B is always 0% with the exception of trading within the own country """ - - country = billing_address.country - - # Need to have a provider country - providers = UncloudProvider.objects.all() vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first() - if not providers and not vatrate: - return 0 - - uncloud_provider = filter_for_when(providers).get() - # By default we charge VAT. This affects: # - Same country sales (VAT applied) # - B2C to EU (VAT applied) rate = vatrate.rate if vatrate else 0 + if not country.lower().strip() in EU_COUNTRIES: + rate = 0 + + return rate + + @staticmethod + def get_vat_rate(billing_address, when=None): + rate = VATRate.get_vat_rate_for_country(billing_address.country, when) # Exception: if... # - the billing_address is in EU, @@ -296,7 +287,7 @@ class VATRate(models.Model): # - the vat_number has been verified # Then we do not charge VAT - if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified: + if rate != 0 and billing_address.vat_number and billing_address.vat_number_verified: rate = 0 return rate @@ -408,7 +399,7 @@ class Product(models.Model): def __str__(self): - return f"{self.name} - {self.description}" + return f"{self.name}" @property def recurring_orders(self): @@ -1010,7 +1001,11 @@ class Bill(models.Model): # FIXME: editable=True -> is in the admin, but also editable in DRF # Maybe filter fields in the serializer? - is_closed = models.BooleanField(default=False) + status = models.CharField(max_length=32, choices= ( + ('new', 'New'), + ('cancelled', 'Cancelled'), + ('paid', 'Paid') + ), null=False, blank=False, default="new") class Meta: constraints = [ @@ -1020,11 +1015,11 @@ class Bill(models.Model): name='one_bill_per_month_per_user') ] - def close(self): + def close(self, status): """ Close/finish a bill """ - self.is_closed = True + self.status = status if not self.ending_date: self.ending_date = timezone.now() self.save() @@ -1120,7 +1115,7 @@ class Bill(models.Model): # Get date & bill from previous bill, if it exists if last_bill: - if not last_bill.is_closed: + if last_bill.status == 'new': bill = last_bill starting_date = last_bill.starting_date ending_date = bill.ending_date diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index 020f359..9f52574 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -122,7 +122,7 @@ class BillSerializer(serializers.ModelSerializer): model = Bill fields = ['owner', 'sum', 'vat_rate', 'due_date', 'creation_date', 'starting_date', 'ending_date', - 'records', 'is_closed', 'billing_address'] + 'records', 'status', 'billing_address'] # We do not want users to mutate the country / VAT number of an address, as it # will change VAT on existing bills. diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 7bfc76b..4f2aded 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -425,7 +425,7 @@ class BillTestCase(TestCase): self.assertEqual(record.quantity, 1) self.assertEqual(record.sum, 35) #close the bill as it has been paid - bill.close() + bill.close(status="paid") bill2 = Bill.create_next_bill_for_user_address(self.user_addr) self.assertNotEqual(bill.id, bill2.id) self.assertEqual(order.billrecord_set.count(), 2) @@ -483,7 +483,7 @@ class BillTestCase(TestCase): for ending_date in self.bill_dates: b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) - b.close() + b.close(status="paid") bill_count = Bill.objects.filter(owner=self.recurring_user).count() @@ -519,14 +519,28 @@ class VATRatesTestCase(TestCase): street="unknown", city="unknown", postal_code="unknown", + country="CH", active=True) UncloudNetwork.populate_db_defaults() UncloudProvider.populate_db_defaults() + VATRate.objects.create(territory_codes="CH", currency_code="CHF", rate=7.7, + starting_date=timezone.make_aware(datetime.datetime(2000,1,1))) + def test_get_rate_for_user(self): """ Raise an error, when there is no address """ + rate = VATRate.get_vat_rate(self.user_addr) + self.assertEqual(rate, 7.7) + self.user_addr.vat_number_verified = True + self.user_addr.vat_number = "11111" + rate1 = VATRate.get_vat_rate(self.user_addr) + self.assertEqual(rate1, 0) + rate2 = VATRate.get_vat_rate_for_country('CH') + self.assertEqual(rate, 7.7) + rate2 = VATRate.get_vat_rate_for_country('EG') + self.assertEqual(rate2, 0) diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py index b657042..d221de6 100644 --- a/uncloud_pay/views.py +++ b/uncloud_pay/views.py @@ -41,11 +41,16 @@ class PricingView(View): vat_rate = False vat_validation_status = False address = False + selected_country = request.GET.get('country', False) if self.request.user and self.request.user.is_authenticated: - address = get_billing_address_for_user(self.request.user) - if address: + address = get_billing_address_for_user(self.request.user) + if address and (address.country == selected_country or not selected_country): vat_rate = VATRate.get_vat_rate(address) vat_validation_status = "verified" if address.vat_number_validated_on and address.vat_number_verified else False + elif selected_country: + vat_rate = VATRate.get_vat_rate_for_country(selected_country) + vat_validation_status = False + pricing = get_order_total_with_vat( request.GET.get('cores'), request.GET.get('memory'),