++bridge update

This commit is contained in:
Nico Schottelius 2021-01-17 15:53:30 +01:00
parent 6b9b15e663
commit a920887100
16 changed files with 275 additions and 138 deletions

0
bin/make-migrations-from-scratch.sh Normal file → Executable file
View file

View file

@ -1,4 +1,4 @@
* Bootstrap / Installation
* Bootstrap / Installation / Deployment
** Pre-requisites by operating system
*** General
To run uncloud you need:
@ -150,7 +150,6 @@ g #+END_SRC
Workers usually should have an "uncloud" user account, even though
strictly speaking the username can be any.
*** WireGuardVPN Server
- Allow write access to /etc/wireguard for uncloud user
- Allow sudo access to "ip" and "wg"
@ -161,7 +160,11 @@ g #+END_SRC
app ALL=(ALL) NOPASSWD:/sbin/ip
app ALL=(ALL) NOPASSWD:/usr/bin/wg
#+END_SRC
** Typical source code based deployment
- Deploy using bin/deploy.sh on a remote server
- Remote server should have
- postgresql running, accessible via TLS from outside
- rabbitmq-configured [in progress]
* Testing / CLI Access
Access via the commandline (CLI) can be done using curl or
@ -462,6 +465,21 @@ Q vpn-2a0ae5c1200.ungleich.ch
- query on that flag
- verify it every time
***** TODO Generating bill for admins/staff
-
**** Bill fixes needed
***** TODO Double bill in bill id
***** TODO Name the currency
***** TODO Maybe remove the chromium pdf rendering artefacts
- date on the top
- title on the top
- filename bottom left
- page number could even stay
***** TODO Try to shorten the timestamp (remove time zone?)
***** TODO Bill date might be required
***** TODO Total and VAT are empty
***** TODO Line below detail/ heading

File diff suppressed because one or more lines are too long

View file

@ -61,7 +61,7 @@ class UncloudAddress(models.Model):
street = models.CharField(max_length=256)
city = models.CharField(max_length=256)
postal_code = models.CharField(max_length=64)
country = CountryField(blank=True)
country = CountryField(blank=False, null=False)
class Meta:
abstract = True

View file

@ -0,0 +1,4 @@
#content {
width: 400px;
margin: auto;
}

View file

@ -2,7 +2,6 @@
{% load bootstrap4 %}
<!doctype html>
<html lang="en">
<head>
@ -15,7 +14,33 @@
{% block header %}{% endblock %}
</head>
<body>
{% block bootstrap4_content %}
{% block bootstrap4_content %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{% url 'uncloudindex' %}">uncloud</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<!-- <li class="nav-item"> -->
<!-- <a class="nav-link" href="/random/">Generate random prefix</a> -->
<!-- </li> -->
{% if user.is_authenticated %}
<span class="navbar-text">Logged in as {{ user }}.</span>
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'login' %}">Login</a>
</li>
{% endif %}
</ul>
</div>
</nav>
<div class="container">
{% block body %}{% endblock %}
</div>

View file

@ -3,8 +3,16 @@
{% block body %}
<div id="content">
<div id="intro" class="row">
<div class=col>
<h1>Welcome to uncloud</h1>
<div id="intro">
</div>
</div>
<div id="intro" class="row">
<div class="col"><h3>About uncloud</h3></div>
<div class="col-8">
<p>
Welcome to uncloud, the Open Source cloud management
system by <a href="https://ungleich.ch">ungleich</a>.
It is an <a href="{% url 'api-root' %}">API</a> driven system with
@ -13,16 +21,45 @@
Framework</a>. You can
freely <a href="https://code.ungleich.ch/uncloud/uncloud/">access
the source code of uncloud</a>.
</p>
</div>
</div>
<div id="howto" class="row">
<div class="col"><h3>Getting started</h3></div>
<div class="col-8">
<p>uncloud is designed to be as easy as possible to use. However,
there are some "real world" requirements that need to be met to
start using uncloud:
<ul>
<li>First you need
to <a href="https://account.ungleich.ch">register an
account</a>. If you already have one, you can
<a href="{% url 'login' %}">login</a>.
<li>If you have forgotten your password or other issues with
logging in, you can contact the ungleich support
via <strong>support at ungleich.ch</strong>.
<div id="creditcards">
<h2>Credit cards</h2>
<div>
<li>Secondy you will need to
<a href="{% url 'billingaddress-list' %}">create a billing
address</a>. This is required for determining the correct
tax.
<li>Next you will need to
<a href="{% url 'cc_register' %}">register a credit card</a>
from which payments can be made. Your credit card will not
be charged without your consent.
</ul>
</div>
</div>
<div id="creditcards" class="row">
<div class="col"><h3>Credit cards</h3></div>
<div class="col-8">
<p>
Credit cards are registered with stripe. We only save a the
last 4 digits and the expiry date of the card to make
identification for you easier.
</div>
<div>
</p>
<ul>
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
(this is required to be done via Javascript so that we never see
@ -35,19 +72,40 @@
then set it on another credit card.
</div>
</div>
<div id="pay">
<h2>Payments and Balance</h2>
To trigger a payment
<div id="pay" class="row">
<div class="col"><h3>Billing Address, Payments and Balance</h3></div>
<div class="col-8">
<p>Billing addresses behave similar to credit cards: you can
have many of them, but only one can be active. The active
billing address is taken for creating new orders.</p>
<p>In uncloud we use the pre-paid model: you can add money to
your account via payments. You can always check your
balance. The products you use will automatically be charged from
your existing balance.
</p>
<p>In the future you will be able opt-in to automatically
recharging your account at a certain time frame or whenever it
is below a certain amount</p>
<ul>
<li><a href="{% url 'billingaddress-list' %}">Create or list
your billing addresses</a>
<li><a href="{% url 'payment-list' %}">Make a payment or list your payments</a>
<li><a href="{% url 'payment-balance-list' %}">Show your balance</a>
</ul>
</div>
<div id="net">
<h2>Networking</h2>
With uncloud you can use a variety of network related services.
</div>
<div id="net" class="row">
<div class="col"><h3>Networking</h3></div>
<div class="col-8">
<p>
With uncloud you can use a variety of network related
services.
</p>
<ul>
<li>You can <a href="{% url 'wireguardvpnnetwork-list' %}">list or
@ -56,7 +114,6 @@
%}">list which network sizes are available</a>
</ul>
</div>
</div>
</div>
{% endblock %}

View file

@ -37,7 +37,7 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc
# Pay
# router.register(r'v1/my/address', payviews.BillingAddressViewSet, basename='billingaddress')
# router.register(r'v1/my/bill', payviews.BillViewSet, basename='bill')
# router.register(r'v1/my/order', payviews.OrderViewSet, basename='order')
# router.register(r'v1/my/payment-method', payviews.PaymentMethodViewSet, basename='payment-method')
@ -49,9 +49,9 @@ router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduc
# router.register(r'v1/admin/vmcluster', vmviews.VMClusterViewSet)
# User/Account
router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin')
router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register')
# router.register(r'v1/my/user', authviews.UserViewSet, basename='user')
# router.register(r'v1/admin/user', authviews.AdminUserViewSet, basename='useradmin')
# router.register(r'v1/user/register', authviews.AccountManagementViewSet, basename='user/register')
################################################################################
@ -65,7 +65,7 @@ router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basenam
router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard')
router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment')
router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance')
router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress')
urlpatterns = [
path(r'api/', include(router.urls), name='api'),

View file

@ -1,13 +1,14 @@
{% extends 'uncloud/base.html' %}
{% load bootstrap4 %}
{% block body %}
<div class="container">
<form method="post">
<h1>Login to uncloud</h1>
<form method="post" class="form">
{% csrf_token %}
{{ form }}
<input type="submit" value="Login">
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
{% endbuttons %}
</form>
</div>
{% endblock %}

View file

@ -47,9 +47,13 @@ class BillAdmin(admin.ModelAdmin):
raise self._get_404_exception(object_id)
output_file = NamedTemporaryFile()
bill_html = render_to_string("bill.html.j2", {'bill': bill,
bill_html = render_to_string(
"uncloud_pay/bill.html.j2",
{
'bill': bill,
'bill_records': bill.billrecord_set.all()
})
}
)
bytestring_to_pdf(bill_html.encode('utf-8'), output_file)
response = FileResponse(output_file, content_type="application/pdf")
@ -63,7 +67,7 @@ class BillAdmin(admin.ModelAdmin):
if bill is None:
raise self._get_404_exception(object_id)
return render(request, 'bill.html.j2',
return render(request, 'uncloud_pay/bill.html.j2',
{'bill': bill,
'bill_records': bill.billrecord_set.all()
})

File diff suppressed because one or more lines are too long

View file

@ -1,24 +1,23 @@
import logging
import itertools
import datetime
from math import ceil
from calendar import monthrange
from decimal import Decimal
from functools import reduce
from django.db import models
from django.db.models import Q
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator
from django.utils import timezone
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
# Verify whether or not to use them here
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
from uncloud.models import UncloudAddress
from .services import *
# Used to generate bill due dates.
BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
@ -26,36 +25,6 @@ BILL_PAYMENT_DELAY=datetime.timedelta(days=10)
# Initialize logger.
logger = logging.getLogger(__name__)
def start_of_month(a_day):
""" Returns first of the month of a given datetime object"""
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
def end_of_month(a_day):
""" Returns first of the month of a given datetime object"""
_, last_day = monthrange(a_day.year, a_day.month)
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
def start_of_this_month():
""" Returns first of this month"""
a_day = timezone.now()
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
def end_of_this_month():
""" Returns first of this month"""
a_day = timezone.now()
_, last_day = monthrange(a_day.year, a_day.month)
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
def end_before(a_date):
""" Return suitable datetimefield for ending just before a_date """
return a_date - datetime.timedelta(seconds=1)
def start_after(a_date):
""" Return suitable datetimefield for starting just after a_date """
return a_date + datetime.timedelta(seconds=1)
def default_payment_delay():
return timezone.now() + BILL_PAYMENT_DELAY
@ -68,7 +37,6 @@ class Currency(models.TextChoices):
# USD = 'USD', _('US Dollar')
###
# Stripe
@ -95,7 +63,7 @@ class StripeCreditCard(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(fields=['owner'],
condition=Q(active=True),
condition=models.Q(active=True),
name='one_active_card_per_user')
]
@ -117,9 +85,7 @@ class Payment(models.Model):
('stripe', 'Stripe'),
('voucher', 'Voucher'),
('referral', 'Referral'),
('unknown', 'Unknown')
),
default='unknown')
))
timestamp = models.DateTimeField(default=timezone.now)
@ -135,6 +101,11 @@ class Payment(models.Model):
class PaymentMethod(models.Model):
"""
Not sure if this is still in use
"""
owner = models.ForeignKey(get_user_model(),
on_delete=models.CASCADE,
editable=False)
@ -151,15 +122,6 @@ class PaymentMethod(models.Model):
stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True)
stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True)
# @property
# def stripe_card_last4(self):
# if self.source == 'stripe' and self.active:
# payment_method = uncloud_pay.stripe.get_payment_method(
# self.stripe_payment_method_id)
# return payment_method.card.last4
# else:
# return None
@property
def active(self):
if self.source == 'stripe' and self.stripe_payment_method_id != None:
@ -276,7 +238,7 @@ class BillingAddress(UncloudAddress):
class Meta:
constraints = [
models.UniqueConstraint(fields=['owner'],
condition=Q(active=True),
condition=models.Q(active=True),
name='one_active_billing_address_per_user')
]
@ -297,18 +259,13 @@ class BillingAddress(UncloudAddress):
if not billing_address:
billing_address = cls.objects.create(owner=owner,
organization="uncloud admins",
name="Uncloud Admin",
full_name="Uncloud Admin",
street="Uncloudstreet. 42",
city="Luchsingen",
postal_code="8775",
country="CH",
active=True)
@staticmethod
def get_address_for(user):
return BillingAddress.objects.get(owner=user, active=True)
def __str__(self):
return "{} - {}, {}, {} {}, {}".format(
self.owner,
@ -1186,7 +1143,7 @@ class Bill(models.Model):
return bill
def __str__(self):
return f"Bill {self.owner}-{self.id}"
return f"{self.owner}-{self.id}"
class BillRecord(models.Model):
@ -1256,7 +1213,7 @@ class ProductToRecurringPeriod(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(fields=['product'],
condition=Q(is_default=True),
condition=models.Q(is_default=True),
name='one_default_recurring_period_per_product'),
models.UniqueConstraint(fields=['product', 'recurring_period'],
name='recurring_period_once_per_product')

View file

@ -21,3 +21,6 @@ def get_spendings_for_user(user):
@transaction.atomic
def get_balance_for_user(user):
return get_payments_for_user(user) - get_spendings_for_user(user)
def get_billing_address_for_user(user):
return BillingAddress.objects.get(owner=user, active=True)

View file

@ -36,6 +36,11 @@ class PaymentSerializer(serializers.ModelSerializer):
class BalanceSerializer(serializers.Serializer):
balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS)
class BillingAddressSerializer(serializers.ModelSerializer):
class Meta:
model = BillingAddress
exclude = [ "owner" ]
################################################################################
# Unchecked code
@ -96,11 +101,6 @@ class BillRecordSerializer(serializers.Serializer):
amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS)
class BillingAddressSerializer(serializers.ModelSerializer):
class Meta:
model = BillingAddress
fields = ['uuid', 'organization', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number']
class BillSerializer(serializers.ModelSerializer):
billing_address = BillingAddressSerializer(read_only=True)
records = BillRecordSerializer(many=True, read_only=True)

32
uncloud_pay/services.py Normal file
View file

@ -0,0 +1,32 @@
from django.utils import timezone
def start_of_month(a_day):
""" Returns first of the month of a given datetime object"""
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
def end_of_month(a_day):
""" Returns first of the month of a given datetime object"""
_, last_day = monthrange(a_day.year, a_day.month)
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
def start_of_this_month():
""" Returns first of this month"""
a_day = timezone.now()
return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0)
def end_of_this_month():
""" Returns first of this month"""
a_day = timezone.now()
_, last_day = monthrange(a_day.year, a_day.month)
return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0)
def end_before(a_date):
""" Return suitable datetimefield for ending just before a_date """
return a_date - datetime.timedelta(seconds=1)
def start_after(a_date):
""" Return suitable datetimefield for starting just after a_date """
return a_date + datetime.timedelta(seconds=1)

View file

@ -680,11 +680,9 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
</div>
<div class="d4">
<div class="b1">
{{ bill.starting_date|date:"c" }} -
{{ bill.ending_date|date:"c" }}
<br>Bill id: {{ bill }}
<br>Due: {{ bill.due_date }}
Bill id: {{ bill }}
<br>{{ bill.starting_date|date:"Ymd" }} -
{{ bill.ending_date|date:"Ymd" }}
</div>
</div>
<div style="clear: both;"></div>
@ -703,8 +701,8 @@ oAsAAAAAAACGQNAFAAAAAAAAQyDoAgAAAAAAgCEQdAEAAAAAAMAQCLoAAAAAAABgCP83AL6WQ1Y7
<tbody>
{% for record in bill_records %}
<tr class="table-list">
<td>{{ record.starting_date|date:"c" }}
- {{ record.ending_date|date:"c" }}
<td>{{ record.starting_date|date:"Ymd-H:i:s" }}
- {{ record.ending_date|date:"Ymd-H:i:s" }}
{{ record.order }}
</td>
<td>{{ record.price|floatformat:2 }}</td>