Initial commit
commit
2f7deaae2d
@ -0,0 +1,91 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
ccvpn/local_settings.py
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 PacketImpact
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,40 @@
|
||||
CCrypto VPN
|
||||
===========
|
||||
|
||||
CCVPN is the software we use at CCrypto to provide our VPN.
|
||||
You can see it live at https://vpn.ccrypto.org/
|
||||
|
||||
It handles user management, support tickets, billing and is used as a backend
|
||||
for VPN authentication.
|
||||
It communicates with an external service, lambdacore, that manages VPN servers
|
||||
and sessions.
|
||||
|
||||
CCrypto's commercial support *does not* include this product and will not help you set it up.
|
||||
Feel free to contact us about ccvpn, but with no guarantee.
|
||||
[PacketImpact](https://packetimpact.net/) however may provide you commercial support
|
||||
and more services about ccvpn and lambdacore.
|
||||
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
```bash
|
||||
git clone https://github.com/CCrypto/ccvpn3.git
|
||||
cd ccvpn3/
|
||||
|
||||
./manage.py createsuperuser
|
||||
./manage.py runserver
|
||||
|
||||
```
|
||||
|
||||
CRON
|
||||
----
|
||||
|
||||
For bitcoin payments, you will need to run a script regularly to check for
|
||||
verified transaction. Another to delete old cancelled payments.
|
||||
And another to send expiration emails.
|
||||
|
||||
*/5 * * * * /home/vpn/ccvpn3/manage.py check_btc_payments
|
||||
0 0 * * * /home/vpn/ccvpn3/manage.py expire_payments
|
||||
0 */6 * * * /home/vpn/ccvpn3/manage.py expire_notify
|
||||
|
@ -0,0 +1,39 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIG0zCCBLugAwIBAgIJAOOv2BdszSOVMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD
|
||||
VQQGEwJGUjERMA8GA1UEBxMIU29tZUNpdHkxHzAdBgNVBAoTFkNvZ25pdGl2ZSBD
|
||||
cnlwdG9ncmFwaHkxEzARBgNVBAsTCkNDcnlwdG9WUE4xEzARBgNVBAMTCkNDcnlw
|
||||
dG9WUE4xEzARBgNVBCkTCkNDcnlwdG9WUE4xHzAdBgkqhkiG9w0BCQEWEGNlcnRA
|
||||
Y2NyeXB0by5vcmcwHhcNMTMwODEzMTgxOTQ4WhcNMjMwODExMTgxOTQ4WjCBoTEL
|
||||
MAkGA1UEBhMCRlIxETAPBgNVBAcTCFNvbWVDaXR5MR8wHQYDVQQKExZDb2duaXRp
|
||||
dmUgQ3J5cHRvZ3JhcGh5MRMwEQYDVQQLEwpDQ3J5cHRvVlBOMRMwEQYDVQQDEwpD
|
||||
Q3J5cHRvVlBOMRMwEQYDVQQpEwpDQ3J5cHRvVlBOMR8wHQYJKoZIhvcNAQkBFhBj
|
||||
ZXJ0QGNjcnlwdG8ub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
|
||||
xvkZj62nUvSjEPs1qBokLd8bBpBlLj6RGgJpfPqS/kKF0s1HpcYZynIcqP6Dw/Pi
|
||||
LFcTE1STzgFfcEdKLmZAH+JCFVpc9mRTXEifouBk+2j3MG9+j2GTXHCK5FMkcJWQ
|
||||
o4YihO2UOLz8qz4yn3dmy0zP1UmqxB2SayYXhwT2+pDSTkBCP6YtRURVVNIVRM7A
|
||||
72hBUJ2dUgKHMTsBJSQj/11rRJ6wW6yUt0NtEcDUbdgrq0BibHq8zzCkl3vGl20M
|
||||
2UCXPNKavP1aapoDGWLSxZgJ9nkFUfWbjWjHuuBw8cjQ7OV7SLiBXqLNJdcCshDA
|
||||
OlwaPS4atao73rliE8bWAsGiwJZ+WXnGNJAr/6BwEtOizc6hr92S8lHOrlEWz3/F
|
||||
h2+L/GI97KMM+pfxlTd8j4dbBpDIXv2vlpYOQ97EbYSbWv7fmYZ2BxxgljATBPfA
|
||||
hA/y7GEfTodC/mkyZO8R2joBxcbQRu7AjsL30AOiE0GepUQNqhlbhEePvm28C9Rn
|
||||
OHm4zaqQLo3BJzlP23N9sn4cMZYMiPnx+eCDv+UW7Y+xHVz0GSjlO1IkZ5lTu2fR
|
||||
IbONfLYDGcByOBaLmo3oD60grw552COTAYDMlu2h8zQV0gPhJziO/txDapoBuNeX
|
||||
XUdzzp1l6GFTsqIKs6ATleJ4n/S4OQe/kicZkCdYZ3kCAwEAAaOCAQowggEGMB0G
|
||||
A1UdDgQWBBTWsE0h2/8fw8SbrhvXny8aBL/KTTCB1gYDVR0jBIHOMIHLgBTWsE0h
|
||||
2/8fw8SbrhvXny8aBL/KTaGBp6SBpDCBoTELMAkGA1UEBhMCRlIxETAPBgNVBAcT
|
||||
CFNvbWVDaXR5MR8wHQYDVQQKExZDb2duaXRpdmUgQ3J5cHRvZ3JhcGh5MRMwEQYD
|
||||
VQQLEwpDQ3J5cHRvVlBOMRMwEQYDVQQDEwpDQ3J5cHRvVlBOMRMwEQYDVQQpEwpD
|
||||
Q3J5cHRvVlBOMR8wHQYJKoZIhvcNAQkBFhBjZXJ0QGNjcnlwdG8ub3JnggkA46/Y
|
||||
F2zNI5UwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAgEAMQsLBf8TPFkh
|
||||
/cnsh+E2N5YCqoeiYwLfPlqo3DwVaDoD8w5NovbIj+O6Z31FkTbt2Zotky6BQMCz
|
||||
5wfuPa1WPuU2kARqS+4LA8FOd1tH4+xqGE6lgkqQuKGJEhi4J4wBg0yKvS4GxZ0+
|
||||
ZXw+fWZSGDPR8WlKVfAR6DEuPIgJqIo0hIWbMgKsmqDzxevWg+Y8C9X0VjLwaSX+
|
||||
dcC9N+ztYFKkzP4yOba8vFjYBmAYzaxwloUmm+iaTD/jrik9kqjd6grNh8bQa/0N
|
||||
hzoa0UL8qgnorusvzo85B/I+cavS37djAeoZ1Oilerp39HS5yjvtbI/ijKW6Wp0l
|
||||
aYCQ6e9f6Ka9cMfVwDJ+aow3rp0MJG0ilP+mS+ouE/R4tnAfVXrlh5X7tsGanByu
|
||||
LtXIbRrLGcCmmNLtdZ2lRVi3BRRKQu5G/6WkvjslBU27CcvKnTtT7FmtKg9564rB
|
||||
NCRawVf3veelwmVdnmkKd6Ka05ymJrm3ZU03+41ilSiqecFRFqz/+urdYkmynJtq
|
||||
E4hNy013h6rooKnNXrMDMuTJqPTa0xJTM+74+JQwEZETSW7TdAGVrqRnJXdXn8Zi
|
||||
17Gr351BJNOBWvyk8MPBKLM24e0qX+4NykQCT8Whhj4YQ/wzvwx2ayKAfoJYd74s
|
||||
9ZCg4dyZMObBjkw08am0H6W6pe9umaY=
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,9 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def some_settings(request):
|
||||
return {
|
||||
'ROOT_URL': settings.ROOT_URL,
|
||||
'ADDITIONAL_HTML': settings.ADDITIONAL_HTML,
|
||||
'ADDITIONAL_HEADER_HTML': settings.ADDITIONAL_HEADER_HTML,
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import hashlib
|
||||
import binascii
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.contrib.auth.hashers import BasePasswordHasher
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
class LegacyPasswordHasher(BasePasswordHasher):
|
||||
""" Legacy password hasher.
|
||||
Single SHA512 iteration with a 32 bytes salt.
|
||||
It's wrong and should not be used except for backward compatibility.
|
||||
CCVPN2 had it in a binary form, it must be base64-encoded and appened
|
||||
to "legacy_sha512$" during the migration.
|
||||
"""
|
||||
algorithm = "legacy_sha512"
|
||||
|
||||
def encode(self, password, salt):
|
||||
assert password is not None
|
||||
if isinstance(password, str):
|
||||
password = bytes(password, 'utf-8')
|
||||
if isinstance(salt, str):
|
||||
salt = bytes(salt, 'utf-8')
|
||||
hash = hashlib.sha512(salt + password)
|
||||
return "%s$%s%s" % (self.algorithm, binascii.b2a_hex(salt).decode('utf-8'),
|
||||
hash.hexdigest())
|
||||
|
||||
def verify(self, password, encoded):
|
||||
algorithm, rest = encoded.split('$', 1)
|
||||
assert algorithm == self.algorithm
|
||||
|
||||
binary = binascii.a2b_hex(rest)
|
||||
|
||||
encoded_2 = self.encode(password, binary[:32])
|
||||
return encoded == encoded_2
|
||||
|
||||
def safe_summary(self, encoded):
|
||||
algorithm, hash = encoded.split('$', 1)
|
||||
assert algorithm == self.algorithm
|
||||
return OrderedDict([
|
||||
(_('algorithm'), algorithm),
|
||||
(_('salt'), hash[0:8]),
|
||||
(_('hash'), hash[64:72]),
|
||||
])
|
||||
|
||||
def must_update(self, encoded):
|
||||
return True # "legacy"
|
||||
|
@ -0,0 +1,173 @@
|
||||
"""
|
||||
Django settings for ccvpn project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.8.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.8/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '@4+zlzju0(wymvatr%8uguuc-aeap8yaz$269ftloqhd&vm%c4'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_countries',
|
||||
'lambdainst',
|
||||
'payments',
|
||||
'tickets',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.common.BrokenLinkEmailsMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'lambdainst.middleware.ReferrerMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'ccvpn.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates/'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.template.context_processors.static',
|
||||
'django.template.context_processors.csrf',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'ccvpn.context_processors.some_settings',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ccvpn.wsgi.application'
|
||||
|
||||
LOGIN_URL = 'account:login'
|
||||
LOGOUT_URL = 'account:logout'
|
||||
|
||||
LOGIN_REDIRECT_URL = 'ccvpn.views.index'
|
||||
LOGOUT_REDIRECT_URL = 'ccvpn.views.index'
|
||||
|
||||
PAGES_DIR = BASE_DIR + '/pages/'
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'ccvpn.passwords.LegacyPasswordHasher',
|
||||
]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = (
|
||||
('fr', "French"),
|
||||
('en', "English"),
|
||||
)
|
||||
|
||||
LOCALE_PATHS = (
|
||||
os.path.join(BASE_DIR, 'locale/'),
|
||||
)
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
# Security
|
||||
|
||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||
|
||||
SESSION_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_SECURE = False
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
|
||||
SECURE_SSL_REDIRECT = False
|
||||
SECURE_HSTS_SECONDS = 3600
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
|
||||
|
||||
# OpenVPN CA Certificate
|
||||
|
||||
with open(BASE_DIR + '/ccvpn/ca.crt') as ca_file:
|
||||
OPENVPN_CA = ca_file.read()
|
||||
|
||||
|
||||
ADDITIONAL_HEADER_HTML = ''
|
||||
ADDITIONAL_HTML = ''
|
||||
|
||||
LAMBDAINST_CLUSTER_MESSAGES = {}
|
||||
|
||||
# Local settings
|
||||
try:
|
||||
from .local_settings import * # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
@ -0,0 +1,38 @@
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
from . import views
|
||||
|
||||
from lambdainst import urls as account_urls, views as account_views
|
||||
from payments import urls as payments_urls
|
||||
from tickets import urls as tickets_urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/status$', account_views.admin_status, name='admin_status'),
|
||||
url(r'^admin/referrers$', account_views.admin_ref, name='admin_ref'),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
url(r'^api/auth$', account_views.api_auth),
|
||||
|
||||
url(r'^$', views.index, name='index'),
|
||||
url(r'^ca.crt$', account_views.ca_crt),
|
||||
url(r'^setlang$', views.set_lang, name='set_lang'),
|
||||
url(r'^chat$', views.chat, name='chat'),
|
||||
url(r'^page/(?P<name>[a-zA-Z0-9_-]+)$', views.page, name='page'),
|
||||
url(r'^status$', account_views.status),
|
||||
|
||||
url(r'^account/forgot$', auth_views.password_reset,
|
||||
{}, name='password_reset'),
|
||||
url(r'^account/forgot_done$', auth_views.password_reset_done,
|
||||
name='password_reset_done'),
|
||||
url(r'^account/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
auth_views.password_reset_confirm, name='password_reset_confirm'),
|
||||
url(r'^account/reset/done/$', auth_views.password_reset_complete,
|
||||
name='password_reset_complete'),
|
||||
|
||||
url(r'^account/', include(account_urls, namespace='account')),
|
||||
url(r'^payments/', include(payments_urls, namespace='payments')),
|
||||
url(r'^tickets/', include(tickets_urls, namespace='tickets')),
|
||||
|
||||
]
|
@ -0,0 +1,82 @@
|
||||
import os.path
|
||||
|
||||
import markdown
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _, get_language
|
||||
from django import http
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.translation import (
|
||||
LANGUAGE_SESSION_KEY, check_for_language,
|
||||
)
|
||||
|
||||
|
||||
md = markdown.Markdown(extensions=['toc', 'meta', 'codehilite(noclasses=True)'])
|
||||
|
||||
|
||||
def index(request):
|
||||
eur = '%.2f' % (settings.PAYMENTS_MONTHLY_PRICE / 100)
|
||||
return render(request, 'ccvpn/index.html', dict(eur_price=eur))
|
||||
|
||||
|
||||
def chat(request):
|
||||
if request.user.is_authenticated():
|
||||
username = request.user.username + '|cc'
|
||||
else:
|
||||
username = "cc?"
|
||||
return render(request, 'ccvpn/chat.html', dict(username=username))
|
||||
|
||||
|
||||
def set_lang(request):
|
||||
""" django.views.i18n.set_language() with GET """
|
||||
|
||||
next = request.GET.get('next', request.GET.get('next'))
|
||||
if not is_safe_url(url=next, host=request.get_host()):
|
||||
next = request.META.get('HTTP_REFERER')
|
||||
if not is_safe_url(url=next, host=request.get_host()):
|
||||
next = '/'
|
||||
response = http.HttpResponseRedirect(next)
|
||||
lang_code = request.GET.get('lang', None)
|
||||
if lang_code and check_for_language(lang_code):
|
||||
if hasattr(request, 'session'):
|
||||
request.session[LANGUAGE_SESSION_KEY] = lang_code
|
||||
else:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code,
|
||||
max_age=settings.LANGUAGE_COOKIE_AGE,
|
||||
path=settings.LANGUAGE_COOKIE_PATH,
|
||||
domain=settings.LANGUAGE_COOKIE_DOMAIN)
|
||||
return response
|
||||
|
||||
|
||||
def page(request, name):
|
||||
basename = settings.PAGES_DIR + '/' + name
|
||||
|
||||
username = request.user.username
|
||||
|
||||
page_replace = {
|
||||
'USERNAME': username or '[username]',
|
||||
}
|
||||
|
||||
files = [
|
||||
basename + '.' + get_language() + '.md',
|
||||
basename + '.en.md',
|
||||
basename + '.md',
|
||||
]
|
||||
|
||||
for file in files:
|
||||
if not os.path.isfile(file):
|
||||
continue
|
||||
|
||||
with open(file, encoding='utf8') as fh:
|
||||
page = fh.read()
|
||||
for s, r in page_replace.items():
|
||||
page = page.replace('{' + s + '}', r)
|
||||
page = md.convert(page)
|
||||
|
||||
title = md.Meta.get('title', [None])[0]
|
||||
ctx = dict(content=page, title=title)
|
||||
return render(request, 'ccvpn/page.html', ctx)
|
||||
|
||||
return HttpResponseNotFound()
|
||||
|
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for ccvpn project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ccvpn.settings")
|
||||
|
||||
application = get_wsgi_application()
|
@ -0,0 +1,132 @@
|
||||
import string
|
||||
from django.shortcuts import resolve_url
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from lambdainst.models import VPNUser, GiftCode, GiftCodeUser
|
||||
|
||||
|
||||
def make_user_link(user):
|
||||
change_url = resolve_url('admin:auth_user_change', user.id)
|
||||
return '<a href="%s">%s</a>' % (change_url, user.username)
|
||||
|
||||
|
||||
class GiftCodeAdminForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
input_code = self.cleaned_data.get('code', '')
|
||||
code_charset = string.ascii_letters + string.digits
|
||||
if any(c not in code_charset for c in input_code):
|
||||
raise forms.ValidationError(_("Code must be [a-zA-Z0-9]"))
|
||||
if not 1 <= len(input_code) <= 32:
|
||||
raise forms.ValidationError(_("Code must be between 1 and 32 characters"))
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class VPNUserInline(admin.StackedInline):
|
||||
model = VPNUser
|
||||
can_delete = False
|
||||
fk_name = 'user'
|
||||
|
||||
fields = ('notes', 'expiration', 'last_expiry_notice', 'notify_expiration',
|
||||
'trial_periods_given', 'referrer_a', 'last_vpn_auth')
|
||||
readonly_fields = ('referrer_a', 'last_vpn_auth')
|
||||
|
||||
def referrer_a(self, object):
|
||||
if not object.referrer:
|
||||
return "-"
|
||||
|
||||
s = make_user_link(object.referrer) + " "
|
||||
if object.referrer_used:
|
||||
s += _("(rewarded)")
|
||||
else:
|
||||
s += _("(not rewarded)")
|
||||
return s
|
||||
referrer_a.allow_tags = True
|
||||
referrer_a.short_description = _("Referrer")
|
||||
|
||||
def is_paid(self, object):
|
||||
return object.is_paid
|
||||
is_paid.boolean = True
|
||||
is_paid.short_description = _("Is paid?")
|
||||
|
||||
|
||||
class GiftCodeUserAdmin(admin.TabularInline):
|
||||
model = GiftCodeUser
|
||||
fields = ('user_link', 'code_link', 'date')
|
||||
readonly_fields = ('user_link', 'code_link', 'date')
|
||||
list_display = ('user', )
|
||||
original = False
|
||||
|
||||
def user_link(self, object):
|
||||
return make_user_link(object.user)
|
||||
user_link.allow_tags = True
|
||||
user_link.short_description = 'User'
|
||||
|
||||
def code_link(self, object):
|
||||
change_url = resolve_url('admin:lambdainst_giftcode_change', object.code.id)
|
||||
return '<a href="%s">%s</a>' % (change_url, object.code.code)
|
||||
code_link.allow_tags = True
|
||||
code_link.short_description = 'Code'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
class UserAdmin(UserAdmin):
|
||||
inlines = (VPNUserInline, GiftCodeUserAdmin)
|
||||
list_display = ('username', 'email', 'is_staff', 'date_joined', 'is_paid')
|
||||
ordering = ('-date_joined', )
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password', 'email', 'links')}),
|
||||
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
|
||||
'groups', 'user_permissions')}),
|
||||
)
|
||||
readonly_fields = ('last_login', 'date_joined', 'links')
|
||||
|
||||
def is_paid(self, object):
|
||||
return object.vpnuser.is_paid
|
||||
is_paid.boolean = True
|
||||
is_paid.short_description = _("Is paid?")
|
||||
|
||||
def links(self, object):
|
||||
fmt = '<a href="%s?user__id__exact=%d">%s</a>'
|
||||
payments_url = resolve_url('admin:payments_payment_changelist')
|
||||
tickets_url = resolve_url('admin:tickets_ticket_changelist')
|
||||
s = fmt % (payments_url, object.id, "Payments")
|
||||
s += ' - ' + fmt % (tickets_url, object.id, "Tickets")
|
||||
return s
|
||||
links.allow_tags = True
|
||||
|
||||
|
||||
class GiftCodeAdmin(admin.ModelAdmin):
|
||||
fields = ('code', 'time', 'created', 'created_by', 'single_use', 'free_only',
|
||||
'available', 'comment')
|
||||
readonly_fields = ('created', 'created_by')
|
||||
list_display = ('code', 'time', 'comment_head', 'available')
|
||||
search_fields = ('code', 'comment', 'users__username')
|
||||
inlines = (GiftCodeUserAdmin,)
|
||||
list_filter = ('available', 'time')
|
||||
form = GiftCodeAdminForm
|
||||
|
||||
def comment_head(self, object):
|
||||
return object.comment_head
|
||||
comment_head.short_description = _("Comment")
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
obj.save()
|
||||
|
||||
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, UserAdmin)
|
||||
|
||||
admin.site.register(GiftCode, GiftCodeAdmin)
|
||||
|
@ -0,0 +1,99 @@
|
||||
from datetime import timedelta, datetime
|
||||
import lcoreapi
|
||||
from django.conf import settings
|
||||
from django.core.mail import mail_admins
|
||||
import logging
|
||||
|
||||
cluster_messages = settings.LAMBDAINST_CLUSTER_MESSAGES
|
||||
|
||||
lcore_settings = settings.LCORE
|
||||
|
||||
LCORE_BASE_URL = lcore_settings.get('BASE_URL')
|
||||
LCORE_API_KEY = lcore_settings['API_KEY']
|
||||
LCORE_API_SECRET = lcore_settings['API_SECRET']
|
||||
LCORE_SOURCE_ADDR = lcore_settings.get('SOURCE_ADDRESS')
|
||||
LCORE_INST_SECRET = lcore_settings['INST_SECRET']
|
||||
|
||||
# The default is to log the exception and only raise it if we cannot show
|
||||
# the previous value or a default value instead.
|
||||
LCORE_RAISE_ERRORS = bool(lcore_settings.get('RAISE_ERRORS', False))
|
||||
|
||||
LCORE_CACHE_TTL = lcore_settings.get('CACHE_TTL', 60)
|
||||
if isinstance(LCORE_CACHE_TTL, int):
|
||||
LCORE_CACHE_TTL = timedelta(seconds=LCORE_CACHE_TTL)
|
||||
assert isinstance(LCORE_CACHE_TTL, timedelta)
|
||||
|
||||
core_api = lcoreapi.API(LCORE_API_KEY, LCORE_API_SECRET, LCORE_BASE_URL)
|
||||
|
||||
|
||||
class APICache:
|
||||
""" Cache data for a time, try to update and silence errors.
|
||||
Outdated data is not a problem.
|
||||
"""
|
||||
def __init__(self, ttl=None, initial=None):
|
||||
self.cache_date = datetime.fromtimestamp(0)
|
||||
self.ttl = ttl or LCORE_CACHE_TTL
|
||||
|
||||
self.has_cached_value = initial is not None
|
||||
self.cached = initial() if initial else None
|
||||
|
||||
def query(self, wrapped, *args, **kwargs):
|
||||
try:
|
||||
return wrapped(*args, **kwargs)
|
||||
except lcoreapi.APIError:
|
||||
logger = logging.getLogger('django.request')
|
||||
logger.exception("core api error")
|
||||
|
||||
if LCORE_RAISE_ERRORS:
|
||||
raise
|
||||
|
||||
if not self.has_cached_value:
|
||||
# We only return a default value if we were given one.
|
||||
# Prevents returning an unexpected None.
|
||||
raise
|
||||
|
||||
# Return previous value
|
||||
return self.cached
|
||||
|
||||
def __call__(self, wrapped):
|
||||
def wrapper(*args, **kwargs):
|
||||
if self.cache_date > (datetime.now() - self.ttl):
|
||||
return self.cached
|
||||
|
||||
self.cached = self.query(wrapped, *args, **kwargs)
|
||||
|
||||
# New results *and* errors are cached
|
||||
self.cache_date = datetime.now()
|
||||
return self.cached
|
||||
return wrapper
|
||||
|
||||
|
||||
@APICache(initial=lambda: 0)
|
||||
def current_active_sessions():
|
||||
return core_api.get(core_api.info['current_instance'] + '/sessions', active=True)['total_count']
|
||||
|
||||
|
||||
@APICache(initial=lambda: [])
|
||||
def get_locations():
|
||||
gateways = core_api.get('/gateways/')
|
||||
locations = {}
|
||||
|
||||
for gw in gateways.list_iter():
|
||||
cc = gw['cluster_name']
|
||||
|
||||
if cc not in locations:
|
||||
locations[cc] = dict(
|
||||
servers=0,
|
||||
bandwidth=0,
|
||||
hostname='gw.' + cc + '.204vpn.net',
|
||||
country_code=cc,
|
||||
message=cluster_messages.get(cc),
|
||||
)
|
||||
|
||||
locations[cc]['servers'] += 1
|
||||
locations[cc]['bandwidth'] += gw['bandwidth']
|
||||
|
||||
locations = sorted(locations.items(), key=lambda x: x[1]['country_code'])
|
||||
return locations
|
||||
|
||||
|
@ -0,0 +1,56 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class FormPureRender:
|
||||
def as_pure_aligned(self):
|
||||
html = ''
|
||||
for f in self:
|
||||
html += '<div class="pure-control-group">\n'
|
||||
html += str(f.label_tag()) + '\n'
|
||||
html += str(f) + '\n'
|
||||
if f.errors:
|
||||
html += str(f.errors) + '\n'
|
||||
html += '</div>\n'
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
class UserField(forms.RegexField):
|
||||
def clean(self, value):
|
||||
super(UserField, self).clean(value)
|
||||
try:
|
||||
User.objects.get(username=value)
|
||||
raise forms.ValidationError(_("Username taken."))
|
||||
except User.DoesNotExist:
|
||||
return value
|
||||
|
||||
|
||||
class SignupForm(forms.Form, FormPureRender):
|
||||
username = UserField(
|
||||
label=_("Username"), min_length=2, max_length=16, regex='^[a-zA-Z0-9_-]+$',
|
||||
widget=forms.TextInput(attrs={'required': 'true',
|
||||
'pattern': '[a-zA-Z0-9_-]{2,32}',
|
||||
'placeholder': _("Username"),
|
||||
'autofocus': 'true'})
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_("Password"),
|
||||
widget=forms.PasswordInput(attrs={'placeholder': _("Anything")})
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=_("Repeat"),
|
||||
widget=forms.PasswordInput(attrs={'placeholder': _("Same Anything")})
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label=_("E-Mail"),
|
||||
widget=forms.EmailInput(attrs={'placeholder': _("E-Mail")}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def clean_password(self):
|
||||
if self.data['password'] != self.data['password2']:
|
||||
raise forms.ValidationError(_("Passwords are not the same"))
|
||||
return self.data['password']
|
||||
|
@ -0,0 +1,126 @@
|
||||
from datetime import timedelta, date
|
||||
|
||||
import pygal
|
||||
|
||||
from .models import User
|
||||
from payments.models import BACKENDS
|
||||
from payments.models import Payment
|
||||
|
||||
|
||||
PERIOD_VERBOSE_NAME = {
|
||||
'y': "per month",
|
||||
'm': "per day",
|
||||
}
|
||||
|
||||
|
||||
def monthdelta(date, delta):
|
||||
m = (date.month + delta) % 12
|
||||
y = date.year + (date.month + delta - 1) // 12
|
||||
if not m:
|
||||
m = 12
|
||||
d = min(date.day, [31, 29 if y % 4 == 0 and not y % 400 == 0
|
||||
else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||
][m - 1])
|
||||
return date.replace(day=d, month=m, year=y)
|
||||
|
||||
|
||||
def last_days(n=30):
|
||||
now = date.today()
|
||||
for i in range(n - 1, -1, -1):
|
||||
yield now - timedelta(days=i)
|
||||
|
||||
|
||||
def last_months(n=12):
|
||||
now = date.today().replace(day=1)
|
||||
for i in range(n - 1, -1, -1):
|
||||
yield monthdelta(now, -i)
|
||||
|
||||
|
||||
def time_filter_future(period, m, df):
|
||||
def _filter(o):
|
||||
if period == 'm':
|
||||
return df(o).date() <= m
|
||||
if period == 'y':
|
||||
return df(o).date().replace(day=1) <= m
|
||||
return _filter
|
||||
|
||||
|
||||
def time_filter_between(period, m, df):
|
||||
def _filter(o):
|
||||
if period == 'm':
|
||||
return df(o).year == m.year and df(o).month == m.month and df(o).day == m.day
|
||||
return df(o).date() <= m and df(o).date() > (m - timedelta(days=1))
|
||||
if period == 'y':
|
||||
return df(o).year == m.year and df(o).month == m.month
|
||||
return (df(o).date().replace(day=1) <= m and
|
||||
df(o).date().replace(day=1) > (m - timedelta(days=30)))
|
||||
return _filter
|
||||
|
||||
|
||||
def users_graph(period):
|
||||
chart = pygal.Line(fill=True, x_label_rotation=75, show_legend=False)
|
||||
chart.title = 'Users %s' % PERIOD_VERBOSE_NAME[period]
|
||||
chart.x_labels = []
|
||||
values = []
|
||||
gen = last_days(30) if period == 'm' else last_months(12)
|
||||
users = User.objects.all()
|
||||
|
||||
for m in gen:
|
||||
filter_ = time_filter_future(period, m, lambda o: o.date_joined)
|
||||
users_filtered = filter(filter_, users)
|
||||
values.append(len(list(users_filtered)))
|
||||
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
|
||||
|
||||
chart.add('Users', values)
|
||||
return chart.render()
|
||||
|
||||
|
||||
def payments_paid_graph(period):
|
||||
chart = pygal.StackedBar(x_label_rotation=75, show_legend=True)
|
||||
chart.x_labels = []
|
||||
gen = list(last_days(30) if period == 'm' else last_months(12))
|
||||
|
||||
chart.title = 'Payments %s in €' % (PERIOD_VERBOSE_NAME[period])
|
||||
|
||||
for m in gen:
|
||||
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
|
||||
|
||||
values = dict()
|
||||
for backend_id, backend in BACKENDS.items():
|
||||
values = []
|
||||
payments = list(Payment.objects.filter(status='confirmed', backend_id=backend_id))
|
||||
|
||||
for m in gen:
|
||||
filter_ = time_filter_between(period, m, lambda o: o.created)
|
||||
filtered = filter(filter_, payments)
|
||||
values.append(sum(u.paid_amount for u in filtered) / 100)
|
||||
|
||||
chart.add(backend_id, values)
|
||||
|
||||
return chart.render()
|
||||
|
||||
|
||||
def payments_success_graph(period):
|
||||
chart = pygal.StackedBar(x_label_rotation=75, show_legend=True)
|
||||
chart.x_labels = []
|
||||
gen = list(last_days(30) if period == 'm' else last_months(12))
|
||||
|
||||
chart.title = 'Successful payments %s' % (PERIOD_VERBOSE_NAME[period])
|
||||
|
||||
for m in gen:
|
||||
chart.x_labels.append('%02d/%02d' % (m.month, m.day))
|
||||
|
||||
values = dict()
|
||||
for backend_id, backend in BACKENDS.items():
|
||||
values = []
|
||||
payments = list(Payment.objects.filter(status='confirmed', backend_id=backend_id))
|
||||
|
||||
for m in gen:
|
||||
filter_ = time_filter_between(period, m, lambda o: o.created)
|
||||
filtered = filter(filter_, payments)
|
||||
values.append(sum(1 for u in filtered))
|
||||
|
||||
chart.add(backend_id, values)
|
||||
|
||||
return chart.render()
|
||||
|
@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from lambdainst.core import core_api
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Get informations about core API"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for k, v in core_api.info.items():
|
||||
print("%s: %s" % (k, v))
|
@ -0,0 +1,56 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db.models import Q, F
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.template.loader import get_template
|
||||
from django.template import Context
|
||||
from django.core.mail import send_mass_mail
|
||||
|
||||
from lambdainst.models import VPNUser
|
||||
|
||||
ROOT_URL = settings.ROOT_URL
|
||||
SITE_NAME = settings.TICKETS_SITE_NAME
|
||||
|
||||
NOTIFY_DAYS_BEFORE = settings.NOTIFY_DAYS_BEFORE
|
||||
assert isinstance(NOTIFY_DAYS_BEFORE, (list, tuple, set))
|
||||
|
||||
|
||||
def get_next_expirations(days=3):
|
||||
""" Gets users whose subscription will expire in some days """
|
||||
|
||||
limit_date = timezone.now() + timedelta(days=days)
|
||||
|
||||
users = VPNUser.objects.exclude(user__email__exact='')
|
||||
|
||||
users = users.filter(expiration__gt=timezone.now()) # Not expired
|
||||
users = users.filter(expiration__lt=limit_date) # Expire in a few days
|
||||
|
||||
# Make sure we dont send the notice twice
|
||||
users = users.filter(Q(last_expiry_notice__isnull=True)
|
||||
| Q(expiration__gt=F('last_expiry_notice')
|
||||
+ timedelta(days=days)))
|
||||
return users
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Notify users near the end of their subscription"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
for v in NOTIFY_DAYS_BEFORE:
|
||||
emails = []
|
||||
qs = get_next_expirations(v)
|
||||
users = list(qs)
|
||||
for u in users:
|
||||
ctx = Context(dict(site_name=SITE_NAME, user=u.user,
|
||||
exp=u.expiration, url=ROOT_URL))
|
||||
text = get_template('lambdainst/mail_expire_soon.txt').render(ctx)
|
||||
emails.append(("CCVPN Expiration", text, from_email, [u.user.email]))
|
||||
print("sending -%d days notify to %s ..." % (v, u.user.email))
|
||||
|
||||
send_mass_mail(emails)
|
||||
qs.update(last_expiry_notice=timezone.now())
|
@ -0,0 +1,113 @@
|
||||
from datetime import timedelta
|
||||
from io import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def get_prev_month(d):
|
||||
if d.month == 1:
|
||||
year = d.year - 1
|
||||
month = 12
|
||||
else:
|
||||
year = d.year
|
||||
month = d.month - 1
|
||||
return d.replace(month=month, year=year)
|
||||
|
||||
|
||||
def should_bill(report, user, time_limit):
|
||||
""" Determines if one user has actually paid for the current month """
|
||||
|
||||
# Here for consistency, should be filtered in the query
|
||||
if not user.vpnuser.expiration or user.vpnuser.expiration < time_limit:
|
||||
return False
|
||||
|
||||
# Replay payments
|
||||
payments = list(user.payment_set.order_by('id').filter(status='confirmed'))
|
||||
paid_expiration = None
|
||||
for p in payments:
|
||||
d = p.confirmed_on or p.created
|
||||
paid_expiration = max(paid_expiration or d, d) + p.time
|
||||
|
||||
# Numbre of days paid after the start of the month
|
||||
# If negative and not filtered with vpnuser.expiration, user was given time.
|
||||
# If positive, user has paid for this time.
|
||||
delta = paid_expiration - time_limit
|
||||
|
||||
if delta < timedelta():
|
||||
report.write("- %s (#%d): %s\n" % (user.username, user.id, -delta))
|
||||
|
||||
return delta > timedelta()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Generate and send a monthly usage report to ADMINS"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
addresses = settings.USAGE_REPORT_DESTINATION
|
||||
|
||||
def format_e(n):
|
||||
return '%.2f%s' % (n / 100, settings.PAYMENTS_CURRENCY[1])
|
||||
|
||||
# Dates
|
||||
end = timezone.now().replace(microsecond=0, second=0, minute=0, hour=0, day=5)
|
||||
start = get_prev_month(end)
|
||||
|
||||
# Filter users
|
||||
filtering_report = StringIO()
|
||||
all_users = User.objects.order_by('id')
|
||||
active_users = all_users.filter(vpnuser__expiration__gt=start)
|
||||
paying_users = active_users.filter(payment__status='confirmed').annotate(Count('payment')).filter(payment__count__gt=0)
|
||||
users = [u for u in paying_users if should_bill(filtering_report, u, start)]
|
||||
|
||||
# Generate report
|
||||
report = "CCVPN Usage Report\n"
|
||||
report += "==================\n\n"
|
||||
|
||||
report += "From: %s\nTo : %s\n\n" % (start, end)
|
||||
|
||||
keys = ('Users', 'Active', 'W/Payment', 'Selected')
|
||||
values = (all_users.count(), active_users.count(), paying_users.count(), len(users))
|
||||
report += " | ".join("%-10s" % s for s in keys) + "\n"
|
||||
report += " | ".join("%-10s" % s for s in values) + "\n"
|
||||
report += "\n"
|
||||
|
||||
user_cost = settings.VPN_USER_COST
|
||||
total_cost = settings.VPN_USER_COST * len(users)
|
||||
|
||||
report += "Billed: %d * %s = %s\n" % (len(users), format_e(user_cost), format_e(total_cost))
|
||||
report += "\n"
|
||||
|
||||
if filtering_report.getvalue():
|
||||
report += "Ignored users:\n"
|
||||
report += filtering_report.getvalue()
|
||||
report += "\n"
|
||||
|
||||
users_text = "\n".join("%s (#%d)" % (u.username, u.id) for u in users)
|
||||
|
||||
subject = "[CCVPN] Usage Report: %s to %s" % (
|
||||
start.strftime('%m/%Y'), end.strftime('%m/%Y'))
|
||||
|
||||
# Send
|
||||
print(report)
|
||||
print("-------")
|
||||
print("Send to: " + ", ".join(a for a in addresses))
|
||||
|
||||
print("Confirm? [y/n] ", end='')
|
||||
i = input()
|
||||
if i.lower().strip() != 'y':
|
||||
return
|
||||
|
||||
for dest in addresses:
|
||||
mail = EmailMessage(subject=subject, body=report,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL, to=[dest])
|
||||
mail.attach('users.txt', users_text, 'text/plain')
|
||||
mail.send()
|
||||
|
||||
print("Sent.")
|
||||
|
||||
|
@ -0,0 +1,42 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from .models import User
|
||||
|
||||
|
||||
class ReferrerMiddleware():
|
||||
def process_request(self, request):
|
||||
if 'ref' in request.GET:
|
||||
id = request.GET['ref']
|
||||
elif 'referrer' in request.COOKIES:
|
||||
id = request.COOKIES['referrer']
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
id = int(id.strip())
|
||||
except (ValueError, TypeError):
|
||||
return
|
||||
|
||||
try:
|
||||
u = User.objects.get(id=id)
|
||||
except User.DoesNotExist:
|
||||
return
|
||||
|
||||
request.session['referrer'] = u.id
|
||||
|
||||
def process_response(self, request, response):
|
||||
id = request.session.get('referrer')
|
||||
if not id:
|
||||
return response
|
||||
|
||||
max_age = 365 * 24 * 60 * 60
|
||||
expires = (datetime.utcnow() + timedelta(seconds=max_age))
|
||||
expires = expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
|
||||
response.set_cookie('referrer', id,
|
||||
max_age=max_age,
|
||||
expires=expires,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
secure=settings.SESSION_COOKIE_SECURE or None)
|
||||
return response
|
||||
|
@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import lambdainst.models
|
||||
from django.conf import settings
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='GiftCode',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||
('code', models.CharField(default=lambdainst.models.random_gift_code, max_length=32)),
|
||||
('time', models.DurationField(default=datetime.timedelta(30))),
|
||||
('created', models.DateTimeField(null=True, auto_now_add=True)),
|
||||
('single_use', models.BooleanField(default=True)),
|
||||
('free_only', models.BooleanField(default=True)),
|
||||
('available', models.BooleanField(default=True)),
|
||||
('comment', models.TextField(blank=True)),
|
||||
('created_by', models.ForeignKey(related_name='created_giftcode_set', null=True, blank=True, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Gift Codes',
|
||||
'verbose_name': 'Gift Code',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GiftCodeUser',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||
('date', models.DateTimeField(null=True, auto_now_add=True)),
|
||||
('code', models.ForeignKey(to='lambdainst.GiftCode')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Gift Code Users',
|
||||
'verbose_name': 'Gift Code User',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VPNUser',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, primary_key=True, verbose_name='ID', auto_created=True)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('expiration', models.DateTimeField(null=True, blank=True)),
|
||||
('last_expiry_notice', models.DateTimeField(null=True, blank=True)),
|
||||
('notify_expiration', models.BooleanField(default=True)),
|
||||
('trial_periods_given', models.IntegerField(default=0)),
|
||||
('last_vpn_auth', models.DateTimeField(null=True, blank=True)),
|
||||
('referrer_used', models.BooleanField(default=False)),
|
||||
('referrer', models.ForeignKey(related_name='referrals', null=True, on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'VPN Users',
|
||||
'verbose_name': 'VPN User',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='giftcode',
|
||||
name='users',
|
||||
field=models.ManyToManyField(through='lambdainst.GiftCodeUser', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -0,0 +1,147 @@
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
assert isinstance(settings.TRIAL_PERIOD, timedelta)
|
||||
assert isinstance(settings.TRIAL_PERIOD_LIMIT, int)
|
||||
|
||||
prng = random.SystemRandom()
|
||||
|
||||
|
||||
def random_gift_code():
|
||||
charset = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
return ''.join([prng.choice(charset) for n in range(10)])
|
||||
|
||||
|
||||
class VPNUser(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("VPN User")
|
||||
verbose_name_plural = _("VPN Users")
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
notes = models.TextField(blank=True)
|
||||
|
||||
expiration = models.DateTimeField(blank=True, null=True)
|
||||
last_expiry_notice = models.DateTimeField(blank=True, null=True)
|
||||
notify_expiration = models.BooleanField(default=True)
|
||||
|
||||
trial_periods_given = models.IntegerField(default=0)
|
||||
|
||||
last_vpn_auth = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
referrer = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL,
|
||||
related_name='referrals')
|
||||
referrer_used = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def is_paid(self):
|
||||
if not self.expiration:
|
||||
return False
|
||||
return self.expiration > timezone.now()
|
||||
|
||||
@property
|
||||
def time_left(self):
|
||||
return timezone.now() - self.expiration
|
||||
|
||||
def add_paid_time(self, time):
|
||||
now = timezone.now()
|
||||
if not self.expiration or self.expiration < now:
|
||||
self.expiration = now
|
||||
self.expiration += time
|
||||
|
||||
def give_trial_period(self):
|
||||
self.add_paid_time(settings.TRIAL_PERIOD)
|
||||
self.trial_periods_given += 1
|
||||
|
||||
@property
|
||||
def can_have_trial(self):
|
||||
if self.trial_periods_given >= settings.TRIAL_PERIOD_LIMIT:
|
||||
return False
|
||||
if self.user.payment_set.filter(status='confirmed').count() > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def remaining_trial_periods(self):
|
||||
return settings.TRIAL_PERIOD_LIMIT - self.trial_periods_given
|
||||
|
||||
def on_payment_confirmed(self, payment):
|
||||
if self.referrer and not self.referrer_used:
|
||||
self.referrer.vpnuser.add_paid_time(timedelta(days=14))
|
||||
self.referrer.vpnuser.save()
|
||||
self.referrer_used = True
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_vpnuser(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
VPNUser.objects.create(user=instance)
|
||||
|
||||
|
||||
class GiftCode(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Gift Code")
|
||||
verbose_name_plural = _("Gift Codes")
|
||||
|
||||
code = models.CharField(max_length=32, default=random_gift_code)
|
||||
time = models.DurationField(default=timedelta(days=30))
|
||||
created = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||
created_by = models.ForeignKey(User, related_name='created_giftcode_set',
|
||||
on_delete=models.CASCADE, null=True, blank=True)
|
||||
single_use = models.BooleanField(default=True)
|
||||
free_only = models.BooleanField(default=True)
|
||||
available = models.BooleanField(default=True)
|
||||
comment = models.TextField(blank=True)
|
||||
users = models.ManyToManyField(User, through='GiftCodeUser')
|
||||
|
||||
def use_on(self, user):
|
||||
if not self.available:
|
||||
return False
|
||||
if self.free_only and user.vpnuser.is_paid:
|
||||
return False
|
||||
|
||||
link = GiftCodeUser(user=user, code=self)
|
||||
link.save()
|
||||
|
||||
user.vpnuser.add_paid_time(self.time)
|
||||
user.vpnuser.save()
|
||||
|
||||
if self.single_use:
|
||||
self.available = False
|
||||
|
||||
self.save()
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def comment_head(self):
|
||||
head = self.comment.split('\n', 1)[0]
|
||||
if len(head) > 80:
|
||||
head = head[:80] + "..."
|
||||
return head
|
||||
|
||||
def __str__(self):
|
||||
return self.code
|
||||
|
||||
|
||||
class GiftCodeUser(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Gift Code User")
|
||||
verbose_name_plural = _("Gift Code Users")
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
code = models.ForeignKey(GiftCode)
|
||||
date = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
||||