initial commit

This commit is contained in:
deflax 2017-03-08 20:53:09 +02:00
commit be76b6622f
103 changed files with 8284 additions and 0 deletions

18
LICENSE Normal file
View file

@ -0,0 +1,18 @@
Copyright (c) 2015-2016 deflax
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgement in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

69
README.md Normal file
View file

@ -0,0 +1,69 @@
# proxmaster-admin
web panel for proxmaster built with Flask
setup nginx vhosts:
example.com.conf:
```
server {
listen 80;
server_name panel.example.com;
root /var/www/html;
location / {
}
}
```
example.com-ssl.conf:
```
server {
listen 443 ssl;
server_name EXAMPLE.com;
ssl_certificate /etc/letsencrypt/live/EXAMPLE.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/EXAMPLE.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/letsencrypt/dhparam.pem;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security max-age=15768000;
location / {
proxy_pass http://127.0.0.1:5000$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /novnc {
alias /home/USER/appserver/noVNC;
}
```
setup db backend:
1. apt-get install postgresql postgresql-contrib libpq-dev
2. sudo -i -u postgres psql
3. create user proxadmin with password 'mypassword';
4. create database proxadmin owner proxadmin encoding 'utf-8';
setup panel
1. adduser USER
2. cd /home/USER
3. virtualenv -p python3 appserver
4. cd appserver
5. git clone git://github.com/kanaka/noVNC
6. git clone https://deflax@bitbucket.org/deflax/proxmaster-panel.git
7. source bin/activate
8. cd proxmaster-panel/ ; pip install -r requirements.txt
9. python3 manage.py db init ; python3 manage.py db migrate -m "init" ; python3 manage.py db upgrade ; python3 manage.py deploy
start:
1. crontab -e
2. @reboot /usr/bin/screen -dmS proxadmin /home/proxadmin/appserver/proxmaster-panel/start.sh

45
example_apache_vhost.conf Normal file
View file

@ -0,0 +1,45 @@
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerAdmin support@example.com
ServerName www.example.com
ServerAlias example.com
WSGIDaemonProcess proxadmin user=proxadmin group=proxadmin threads=5
WSGIScriptAlias / /home/proxadmin/appserver/proxmaster-panel/start.wsgi
<Directory "/home/proxadmin/appserver/proxmaster-panel">
<Files "start.wsgi">
Require all granted
</Files>
</Directory>
<Directory "/home/proxadmin/appserver/proxmaster-panel/app">
WSGIProcessGroup proxadmin
WSGIApplicationGroup %{GLOBAL}
WSGIScriptReloading On
Require all granted
Order allow,deny
Allow from all
</Directory>
Alias /static /home/proxadmin/appserver/proxmaster-panel/app/static
<Directory "/home/proxadmin/appserver/proxmaster-panel/app/static">
Require all granted
Order allow,deny
Allow from all
</Directory>
ErrorLog ${APACHE_LOG_DIR}/www.example.com-error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog ${APACHE_LOG_DIR}/www.example.com-access.log combined
Include /etc/letsencrypt/options-ssl-apache.conf
SSLCertificateFile /etc/letsencrypt/live/www.example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/www.example.com/chain.pem
</VirtualHost>
</IfModule>

76
manage.py Normal file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python
import os
import subprocess, shlex
from proxadmin import app, db
from flask_script import Manager, Shell, Command
from flask_migrate import Migrate, MigrateCommand
def make_shell_context():
return dict(app=app,
db=db,
User=User,
Role=Role,
Permission=Permission,
Deployment=Deployment)
migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
@manager.command
def deploy():
"""Run deployment tasks."""
from flask_migrate import upgrade
from app.models import Role, User, Deployment, Product
# migrate database to latest revision
upgrade()
# create user roles
Role.insert_roles()
Product.insert_products()
@manager.command
@manager.option('-r' '--restore_file', help='Restore from grid dump file')
def restore(restore_file):
""" recreate db from grid export with python3 manage.py restore /path/grid.tar.bz2 """
print(str(restore_file))
#TODO
from app.models import User
db.session.add(User(email=str(user), password=str(password), confirmed=True, confirmed_on=datetime.datetime.now()))
db.session.commit()
def run_scheduler():
command_line = 'python3 /home/proxadmin/appserver/proxmaster-panel/schedulerd.py'
args = shlex.split(command_line)
p = subprocess.Popen(args)
@manager.command
def charge_deployments():
from app.models import Product, Deployment, User
Deployment.charge()
@manager.command
def charge_contracts():
from app.models import Service, Contract, User
Contract.charge()
@manager.command
def charge_domains():
from app.models import Domain, User
Domain.charge()
@manager.command
def runserver():
print('Starting Scheduler...')
#run_scheduler()
print('Starting Flask...')
app.run()
if __name__ == '__main__':
manager.run()

103
proxadmin/__init__.py Normal file
View file

@ -0,0 +1,103 @@
from flask import Flask, g, render_template, request
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_wtf.csrf import CSRFProtect, CSRFError
from flask_babel import Babel
from werkzeug.contrib.fixers import ProxyFix
from proxadmin import configure_app
app = Flask(__name__)
configure_app(app)
app.wsgi_app = ProxyFix(app.wsgi_app) #trusting headers behind proxy
db = SQLAlchemy(session_options = { "autoflush": False })
lm = LoginManager()
lm.login_view = 'auth.login'
lm.login_message = 'Login Required.'
lm.session_protection = 'strong'
#lm.session_protection = 'basic'
mail = Mail()
bootstrap = Bootstrap()
#csrf = CSRFProtect()
babel = Babel()
app = Flask(__name__)
app.config.from_object('config')
#if app.debug:
# pass
# #toolbar = DebugToolbarExtension(app)
#else:
# import logging
# from logging.handlers import RotatingFileHandler
# file_handler = RotatingFileHandler('app/log/proxadmin.log', 'a', 1 * 1024 * 1024, 10)
# file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
# app.logger.setLevel(logging.INFO)
# file_handler.setLevel(logging.INFO)
# app.logger.addHandler(file_handler)
# app.logger.info('Proxadmin started.')
bootstrap.init_app(app)
mail.init_app(app)
db.init_app(app)
lm.init_app(app)
babel.init_app(app)
#csrf.init_app(app)
from .proxadmin import proxadmin as proxadmin_blueprint
app.register_blueprint(proxadmin_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
from .uinvoice import uinvoice as uinvoice_blueprint
app.register_blueprint(uinvoice_blueprint, url_prefix='/uinvoice')
@app.errorhandler(403)
def forbidden(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'forbidden'})
response.status_code = 403
return response
return render_template('errors/403.html'), 403
@app.errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'internal server error'})
response.status_code = 500
return response
return render_template('errors/500.html'), 500
@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return render_template('errors/csrf_error.html', reason=e.description), 400
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['SUPPORTED_LOCALES'])
#@app.before_request
#def before_request():
# g.request_start_time = time.time()
# g.request_time = lambda: '%.5fs' % (time.time() - g.request_start_time)
# g.pjax = 'X-PJAX' in request.headers
if __name__ == '__main__':
app.run()

View file

@ -0,0 +1,4 @@
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import routes

51
proxadmin/auth/forms.py Normal file
View file

@ -0,0 +1,51 @@
from flask_wtf import FlaskForm, RecaptchaField
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
from wtforms import validators, ValidationError
from wtforms.fields.html5 import EmailField
from ..models import User
class LoginForm(FlaskForm):
email = EmailField('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
password = PasswordField('Парола', [validators.DataRequired(), validators.Length(1,128)])
remember_me = BooleanField('Запомни ме')
#recaptcha = RecaptchaField()
submit = SubmitField('Вход')
class TwoFAForm(FlaskForm):
token = StringField('Token', [validators.DataRequired(), validators.Length(6, 6)])
submit = SubmitField('Потвърди кода')
class RegistrationForm(FlaskForm):
email = StringField('Електронна Поща', [validators.DataRequired(), validators.Length(6,35), validators.Email()])
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Грешка. Опитайте пак с друг email адрес.')
password = PasswordField('Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')])
confirm = PasswordField('Повторете паролата', [validators.DataRequired()])
accept_tos = BooleanField('Приемам <a href="/terms">Условията за Използване</a> на услугата', [validators.DataRequired()])
recaptcha = RecaptchaField()
submit = SubmitField('Регистрация')
class ChangePasswordForm(FlaskForm):
old_password = PasswordField('Стара парола', [validators.DataRequired()])
password = PasswordField('Нова Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')])
confirm = PasswordField('Повторете паролата')
submit = SubmitField('Обнови паролата')
class PasswordResetRequestForm(FlaskForm):
email = EmailField('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
recaptcha = RecaptchaField()
submit = SubmitField('Възстановяване на парола', [validators.DataRequired()])
class PasswordResetForm(FlaskForm):
email = EmailField('Електронна Поща', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
password = PasswordField('Парола', [validators.DataRequired(), validators.EqualTo('confirm', message='Паролите трябва да съвпадат')])
confirm = PasswordField('Повторете паролата', [validators.DataRequired()])
submit = SubmitField('Промяна на паролата')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Грешка. Опитайте пак с друг email адрес.')

220
proxadmin/auth/routes.py Normal file
View file

@ -0,0 +1,220 @@
from flask import render_template, redirect, request, url_for, flash, session, abort, current_app
from flask_login import login_required, login_user, logout_user, current_user
from . import auth
from .. import db
from ..models import User
from ..email import send_email
from .forms import LoginForm, TwoFAForm, RegistrationForm, ChangePasswordForm,PasswordResetRequestForm, PasswordResetForm
from io import BytesIO
import pyqrcode
@auth.before_app_request
def before_request():
#print('session: %s' % str(session))
if current_user.is_authenticated:
current_user.ping()
#print('request for {} from {}#{}'.format(request.endpoint, current_user.email, current_user.pid))
if not current_user.confirmed and request.endpoint[:5] != 'auth.' and request.endpoint != 'static':
print(request.endpoint)
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('proxadmin.index'))
return render_template('auth/unconfirmed.html')
@auth.route('/login', methods=['GET', 'POST'])
def login():
page = { 'title': 'Login' }
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
if user.twofactor:
# redirect to the two-factor auth page, passing username in session
session['email'] = user.email
session['memberberry'] = form.remember_me.data
return redirect(url_for('auth.twofactor'))
#print('remember: ' + str(form.remember_me.data))
login_user(user, form.remember_me.data)
if request.headers.getlist("X-Forwarded-For"):
lastip = request.headers.getlist("X-Forwarded-For")[0]
else:
lastip = request.remote_addr
user.last_ip = lastip
db.session.add(user)
db.session.commit()
#send_email(current_app.config['MAIL_USERNAME'], user.email + ' logged in.', 'auth/email/adm_loginnotify', user=user, ipaddr=lastip )
return redirect(request.args.get('next') or url_for('proxadmin.dashboard'))
flash('Invalid username or password.')
return render_template('auth/login.html', page=page, form=form)
@auth.route('/twofactor', methods=['GET', 'POST'])
def twofactor():
if 'email' not in session:
abort(404)
if 'memberberry' not in session:
abort(404)
page = { 'title': '2-Factor Login' }
form = TwoFAForm()
if form.validate_on_submit():
user = User.query.filter_by(email=session['email']).first()
del session['email']
if user is not None and user.verify_totp(form.token.data):
print('remember: ' + str(session['memberberry']))
login_user(user, session['memberberry'])
del session['memberberry']
if request.headers.getlist("X-Forwarded-For"):
lastip = request.headers.getlist("X-Forwarded-For")[0]
else:
lastip = request.remote_addr
user.last_ip = lastip
db.session.add(user)
db.session.commit()
#send_email(current_app.config['MAIL_USERNAME'], user.email + ' logged in.', 'auth/email/adm_loginnotify', user=user, ipaddr=lastip )
return redirect(request.args.get('next') or url_for('proxadmin.dashboard'))
return render_template('auth/2fa.html', page=page, form=form)
@auth.route('/qrcode')
@login_required
def qrcode():
#if 'email' not in session:
# abort(404)
#user = User.query.filter_by(email=session['email']).first()
#if user is None:
# abort(404)
# for added security, remove username from session
#del session['email']
# render qrcode for FreeTOTP
url = pyqrcode.create(current_user.get_totp_uri())
stream = BytesIO()
url.svg(stream, scale=6)
return stream.getvalue(), 200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'}
@auth.route("/logout", methods=['GET'])
@login_required
def logout():
logout_user()
flash('You have logged out')
return redirect(url_for('proxadmin.index'))
@auth.route('/register', methods=['GET', 'POST'])
def register():
#print(current_app.secret_key)
page = { 'title': 'Register' }
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data, password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Потвърдетеашият_акаунт', 'auth/email/confirm', user=user, token=token)
#notify admin
newip = request.remote_addr
if request.headers.getlist("X-Forwarded-For"):
newip = request.headers.getlist("X-Forwarded-For")[0]
else:
newip = request.remote_addr
send_email(current_app.config['MAIL_USERNAME'], user.email + ' registered!', 'auth/email/adm_regnotify', user=user, ipaddr=newip )
flash('Благодарим за регистрацията! Моля проверете вашият email за потвърждение')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', page=page, form=form)
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('proxadmin.index'))
if current_user.confirm(token):
flash('Вашият акаунт е потвърден. Благодаря!')
else:
flash('Времето за потвърждение на вашият код изтече.')
return redirect(url_for('proxadmin.index'))
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Потвърдетеашият_акаунт',
'auth/email/confirm', user=current_user, token=token)
flash('Изпратен е нов код за потвърждение..')
return redirect(url_for('proxadmin.index'))
@auth.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
if current_user.verify_password(form.old_password.data):
current_user.password = form.password.data
db.session.add(current_user)
db.session.commit()
flash('Вашата парола беше променена.')
return redirect(url_for('proxadmin.index'))
else:
flash('Грешна парола.')
return render_template("auth/change_password.html", form=form)
@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
if not current_user.is_anonymous:
return redirect(url_for('proxadmin.index'))
form = PasswordResetRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
token = user.generate_reset_token()
send_email(user.email, 'Reset Your Password',
'auth/email/reset_password',
user=user, token=token,
next=request.args.get('next'))
flash('An email with instructions to reset your password has been '
'sent to you.')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous:
return redirect(url_for('proxadmin.index'))
form = PasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
return redirect(url_for('proxadmin.index'))
if user.reset_password(token, form.password.data):
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('proxadmin.index'))
return render_template('auth/reset_password.html', form=form)

20
proxadmin/decorators.py Normal file
View file

@ -0,0 +1,20 @@
from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ADMINISTER)(f)

20
proxadmin/email.py Normal file
View file

@ -0,0 +1,20 @@
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import mail
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
app = current_app._get_current_object()
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + ' ' + subject, sender=app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr

2
proxadmin/exceptions.py Normal file
View file

@ -0,0 +1,2 @@
class ValidationError(ValueError):
pass

415
proxadmin/models.py Normal file
View file

@ -0,0 +1,415 @@
import hashlib
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from config import Config
from flask import current_app, request, url_for
from flask_login import UserMixin, AnonymousUserMixin
from app.exceptions import ValidationError
from . import db, lm
import os
import base64
import json
import requests
import onetimepass
from datetime import date, datetime, time, timedelta
from sortedcontainers import SortedDict
class Permission:
DEPLOY = 0x01
ADMINISTER = 0x80
class Role(db.Model):
__tablename__ = 'roles'
pid = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')
@staticmethod
def insert_roles():
roles = {
'User': (Permission.DEPLOY, True),
'Administrator': (0xff, False)
}
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
role.permissions = roles[r][0]
role.default = roles[r][1]
db.session.add(role)
db.session.commit()
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model, UserMixin):
__tablename__ = 'users'
pid = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.pid')) #FK
password_hash = db.Column(db.String(128))
confirmed = db.Column(db.Boolean, default=False)
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
last_ip = db.Column(db.String(128))
twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth
otp_secret = db.Column(db.String(16))
avatar_hash = db.Column(db.String(32))
name = db.Column(db.Unicode(256))
address = db.Column(db.Unicode(256))
city = db.Column(db.Unicode(64))
postcode = db.Column(db.String(10))
country = db.Column(db.String(64))
phone = db.Column(db.String(64))
org_responsible = db.Column(db.Unicode(128))
org_bulstat = db.Column(db.String(16))
inv_deployments = db.relationship('Deployment', backref='owner', lazy='dynamic')
inv_contracts = db.relationship('Contract', backref='owner', lazy='dynamic')
inv_domains = db.relationship('Domain', backref='owner', lazy='dynamic')
currency = db.Column(db.String(3), default='BGN')
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['ADMIN_EMAIL']:
#if email match config admin name create admin user
self.role = Role.query.filter_by(permissions=0xff).first()
if self.role is None:
#if role is stil not set, create default user role
self.role = Role.query.filter_by(default=True).first()
if self.email is not None and self.avatar_hash is None:
self.avatar_hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
if self.otp_secret is None:
# generate a random secret
self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8')
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def get_totp_uri(self):
return 'otpauth://totp/DataPanel:{0}?secret={1}&issuer=datapanel'.format(self.email, self.otp_secret)
def verify_totp(self, token):
return onetimepass.valid_totp(token, self.otp_secret)
def generate_confirmation_token(self, expiration=86400):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.pid})
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.pid:
return False
self.confirmed = True
db.session.add(self)
db.session.commit()
return True
def generate_reset_token(self, expiration=86400):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.pid})
def reset_password(self, token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.pid:
return False
self.password = new_password
db.session.add(self)
db.session.commit()
return True
def can(self, permissions):
return self.role is not None and (self.role.permissions & permissions) == permissions
def is_administrator(self):
return self.can(Permission.ADMINISTER)
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
db.session.commit()
def gravatar(self, size=100, default='identicon', rating='g'):
#this check is disabled because it didnt work for me but forcing https to gravatar is okay.
#if request.is_secure:
# url = 'https://secure.gravatar.com/avatar'
#else:
# url = 'http://www.gravatar.com/avatar'
url = 'https://secure.gravatar.com/avatar'
hash = self.avatar_hash or hashlib.md5(self.email.encode('utf-8')).hexdigest()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(url=url, hash=hash, size=size, default=default, rating=rating)
def is_authenticated(self):
return self.is_authenticated
def get_id(self):
return str(self.pid)
def __repr__(self):
return '<User %r>' % self.email
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
lm.anonymous_user = AnonymousUser
@lm.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
def contact_proxmaster(data, method, cubeid=0):
url = current_app.config['PROXMASTER_URL']
data['apikey'] = current_app.config['APIKEY']
data_json = json.dumps(data)
#print('--> {}'.format(data))
if method == 'vmcreate':
url = '{}/{}'.format(url, method)
else:
url = '{}/{}/{}'.format(url, method, cubeid)
db_result = requests.post( url, data=data_json, headers={"content-type": "application/json"}, timeout=30 )
try:
proxjson = db_result.json()
#print('proxmaster query {}'.format(str(proxjson)))
return proxjson
except:
return None
#TEMPLATE CLASSES
class Product(db.Model):
__tablename__ = 'products'
pid = db.Column(db.Integer, primary_key=True) #PK
group = db.Column(db.Integer)
name = db.Column(db.String(64))
image = db.Column(db.String(128))
description = db.Column(db.String(128))
cpu = db.Column(db.Integer) #default cpu
mem = db.Column(db.Integer) #default mem
hdd = db.Column(db.Integer) #default hdd
recipe = db.Column(db.String(128)) #defaut template name
enabled = db.Column(db.Boolean)
@staticmethod
def insert_products():
products = current_app.config['PRODUCTS']
for p in products:
product = Product.query.filter_by(pid=p).first()
if product is None:
product = Product(name=p)
#insert default values
product.group = products[p][0]
product.name = products[p][1]
product.image = products[p][2]
product.description = products[p][3]
product.cpu = products[p][4]
product.mem = products[p][5]
product.hdd = products[p][6]
product.recipe = products[p][7]
product.enabled = products[p][8]
db.session.add(product)
db.session.commit()
@staticmethod
def get_products():
result = Product.query.all()
products = {}
for product in result:
if product.enabled == True:
products[int(product.pid)] = { 'group': product.group,
'img': '/static/images/' + product.image,
'name': product.name,
'description': product.description,
'cpu': product.cpu,
'mem': product.mem,
'hdd': product.hdd,
'recipe': product.recipe,
}
return products
class Service(db.Model):
__tablename__ = 'services'
pid = db.Column(db.Integer, primary_key=True) #PK
name = db.Column(db.String(64))
image = db.Column(db.String(128))
description = db.Column(db.String(128))
unitprice = db.Column(db.Float)
enabled = db.Column(db.Boolean)
@staticmethod
def insert_services():
services = current_app.config['SERVICES']
for s in services:
service = Service.query.filter_by(pid=p).first()
if service is None:
service = Service(name=s)
#insert default values
service.name = products[p][1]
service.image = products[p][2]
service.description = products[p][3]
service.unitprice = products[p][4]
service.enabled = products[p][5]
db.session.add(service)
db.session.commit()
@staticmethod
def get_services():
result = Service.query.all()
services = {}
for service in result:
if service.enabled == True:
services[int(service.pid)] = {'img': '/static/images/' + service.image,
'name': service.name,
'description': service.description,
'unitprice': service.unitprice
}
return services
class Order(db.Model):
__tablename__ = 'orders'
pid = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
units = db.Column(db.Integer, default=1)
unitvalue = db.Column(db.Float)
currency = db.Column(db.String, default='BGN')
paid = db.Column(db.Boolean, default=False)
#class Invoice(db.Model)
#INVENTORY CLASSES
class Deployment(db.Model):
__tablename__ = 'deployments'
pid = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
product_id = db.Column(db.Integer, db.ForeignKey('products.pid')) #FK
date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
date_expire = db.Column(db.DateTime)
enabled = db.Column(db.Boolean)
machine_id = db.Column(db.BigInteger) #cubeid
machine_alias = db.Column(db.String) #dns name for easy managing
machine_cpu = db.Column(db.Integer)
machine_mem = db.Column(db.Integer)
machine_hdd = db.Column(db.Integer)
credit = db.Column(db.Float)
def charge():
result = Deployment.query.all()
for deploy in result:
managed_user = User.query.get(deploy.user_id)
db.session.add(deploy)
if datetime.utcnow.date() > (deploy.date_expire - timedelta(days=10)):
if deploy.credit > 0:
print('{} Deployment {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, deploy.machine_alias, deploy.date_expire))
current_product = Product.query.get(int(deploy.product_id))
order = Order(user_id=managed_user.pid, unitvalue=deploy.credit, description='Cloud server {} - {}'.format(current_product.name, deploy.machine_alias))
db.session.add(order)
deploy.credit = 0
deploy.data_expire = datetime.utcnow.date() + timedelta(days=30)
else:
cpu_cost = deploy.machine_cpu * current_app.config['CPU_RATIO']
mem_cost = ( deploy.machine_mem / 1024 ) * current_app.config['MEM_RATIO']
hdd_cost = deploy.machine_hdd * current_app.config['HDD_RATIO']
total = cpu_cost + mem_cost + hdd_cost
if deploy.enabled == True:
print('{}> Charging deployment #{} with {} ({} monthly)'.format(managed_user.email, deploy.pid, total))
deploy.credit += total
db.session.commit()
return True
class Contract(db.Model):
__tablename__ = 'contracts'
pid = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
service_id = db.Column(db.Integer, db.ForeignKey('services.pid')) #FK
date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
date_expire = db.Column(db.DateTime)
enabled = db.Column(db.Boolean)
description = db.Column(db.String)
units = db.Column(db.Integer)
discount = db.Column(db.Integer) #percent
credit = db.Column(db.Float)
def charge():
result = Contract.query.all()
for contract in result:
managed_user = User.query.get(contract.user_id)
db.session.add(contract)
if datetime.utcnow.date() > (contract.date_expire - timedelta(days=10)):
if contract.enabled == True:
print('{}> Contract {} will expire in 10 days at {}. Creating new order...'.format(managed_user.email, contract.pid, contract.date_expire))
current_service = Service.query.get(int(contract.product_id))
order = Order(user_id=managed_user.id, units=contract.units, unitvalue=(current_service.unitprice * contract.units), description=current_service.name)
db.session.add(order)
contract.data_expire = datetime.utcnow.date() + timedelta(days=30)
db.session.commit()
return True
class Domain(db.Model):
__tablename__ = 'domains'
pid = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK
date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow)
date_expire = db.Column(db.DateTime)
enabled = db.Column(db.Boolean)
fqdn = db.Column(db.String, unique=True)
auto_update = db.Column(db.Boolean)
credit = db.Column(db.Float)
def charge():
result = Domain.query.all()
for domain in result:
managed_user = User.query.get(domain.user_id)
db.session.add(domain)
if datetime.utcnow.date() > (domain.date_expire - timedelta(days=60)):
if domain.enabled == True:
print('{}> Domain {} will expire in 60 days at {}. Creating new order...'.format(managed_user.email, domain.fqdn, domain.date_expire))
order = Order(user_id=managed_user.id, unitvalue=25, description=domain.fqdn)
db.session.add(order)
db.session.commit()
return True

View file

@ -0,0 +1,3 @@
from flask import Blueprint
proxadmin = Blueprint('proxadmin', __name__)
from . import routes

View file

@ -0,0 +1,33 @@
from flask import render_template, request, jsonify
from . import proxadmin
@proxadmin.app_errorhandler(403)
def forbidden(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'forbidden'})
response.status_code = 403
return response
return render_template('403.html'), 403
@proxadmin.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
@proxadmin.app_errorhandler(500)
def internal_server_error(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'internal server error'})
response.status_code = 500
return response
return render_template('500.html'), 500

View file

@ -0,0 +1,24 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
from wtforms import validators, ValidationError
from wtforms.fields.html5 import EmailField
class DeployForm(FlaskForm):
servername = StringField('Име/Домейн:', [validators.Regexp(message='пример: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$')])
region = SelectField('Регион:')
vmpassword = StringField('Парола:', [validators.DataRequired()])
cpu = StringField('Процесорни ядра:')
mem = StringField('Памет:')
hdd = StringField('Дисково пространство:')
recipe = SelectField('Рецепта')
#ipv4 = SelectField('Брой публични IP адреса', choices=[('1', '1'),('2', '2' ), ('3', '3')])
invite_key = StringField('Покана', [validators.DataRequired(), validators.Length(6,35)])
def validate_invite_key(self, field):
if field.data != 'inv1919':
raise ValidationError('Denied')
submit = SubmitField('Deploy')

View file

@ -0,0 +1,245 @@
from config import Config
from flask import render_template, abort, redirect, url_for, abort, flash, request, current_app, make_response, g
from flask_login import login_required, login_user, logout_user, current_user
from flask_sqlalchemy import get_debug_queries
from . import proxadmin
from .forms import DeployForm
from .. import db
from ..email import send_email
from ..models import User, Role, Product, Deployment, contact_proxmaster, Contract, Domain
from ..decorators import admin_required, permission_required
import base64
import string
import random
from datetime import datetime, timedelta, date, time
def randstr(n):
return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(n))
#@proxadmin.before_app_request
#def before_request():
# g.user = current_user
# print('current_user: %s, g.user: %s, leaving bef_req' % (current_user, g.user))
@proxadmin.after_app_request
def after_request(response):
for query in get_debug_queries():
if query.duration >= current_app.config['SLOW_DB_QUERY_TIME']:
current_app.logger.warning('Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement, query.parameters, query.duration, query.context))
return response
#STATIC PAGES
@proxadmin.route("/", methods=['GET'])
def index():
return render_template('proxadmin/index.html')
@proxadmin.route("/chat", methods=['GET'])
def chat():
return render_template('proxadmin/livechat.html')
@proxadmin.route("/aboutus", methods=['GET'])
def about():
return render_template('proxadmin/aboutus.html')
@proxadmin.route("/terms", methods=['GET'])
def terms():
return render_template('proxadmin/terms.html')
#APP STORE
@proxadmin.route('/market/<int:group_id>', methods=['GET'])
@login_required
def market(group_id=0):
page = { 'title': 'Market' }
allproducts = Product.get_products()
allgroups = current_app.config['GROUPS']
if group_id == 0:
return render_template('proxadmin/market.html', groups=allgroups, products=allproducts)
filtered_products = {}
for key, value in allproducts.items():
if value['group'] == group_id:
filtered_products[key] = value
if filtered_products == {}:
abort(404)
return render_template('proxadmin/marketgroup.html', groupname=allgroups[group_id], products=filtered_products)
@proxadmin.route('/deploy/<int:product_id>', methods=['GET', 'POST'])
@login_required
def deploy(product_id=None):
#if current_user.wallet < 20:
# flash('Недостатъчно средства в сметката за тази операция')
# return redirect(url_for('uinvoice.addfunds'))
if current_user.name is None:
flash('Моля обновете информацията за профила')
return redirect(url_for('uinvoice.profile'))
page = { 'title': 'Deploy' }
try:
product = Product.get_products()[product_id]
except:
print('unknown product {}'.format(product_id))
abort(404)
product_pic = '..' + product['img']
product_name = product['name']
product_description = product['description']
product_cpu = product['cpu']
product_mem = product['mem']
product_hdd = product['hdd']
product_recipe = product['recipe']
hostname = 'deploy-{}.local'.format(randstr(6))
form = DeployForm(servername=hostname, cpu=product_cpu, mem=product_mem, hdd=product_hdd, recipe=product_recipe)
form.region.choices = current_app.config['REGIONS']
form.recipe.choices = current_app.config['RECIPES']
if current_user.confirmed and form.validate_on_submit():
client_id = current_user.pid
data = { 'clientid': str(client_id),
'clientname': str(current_user.name),
'clientemail': str(current_user.email),
'hostname': str(form.servername.data),
'vmpass': form.vmpassword.data,
'region': form.region.data,
'vps_type': 'kvm',
'vps_recipe': form.recipe.data,
'vps_cpu': form.cpu.data,
'vps_mem': form.mem.data,
'vps_hdd': form.hdd.data,
'vps_ipv4': '1' }
try:
query = contact_proxmaster(data, 'vmcreate')
except:
flash('Region unreachable! Please try again later...')
return redirect(url_for('proxadmin.index'))
if query is not None:
cubeid = query['cube']
deployment = Deployment(user_id=client_id, product_id=product_id, machine_alias=form.servername.data, machine_id=cubeid, machine_cpu=form.cpu.data, machine_mem=form.mem.data, machine_hdd=form.hdd.data, credit=0, date_expire=(datetime.utcnow() + timedelta(days=30)), enabled=True)
db.session.add(deployment)
db.session.commit()
flash('Deploy requested.')
else:
flash('Deploy cancelled! Please try again later...')
return redirect(url_for('proxadmin.index'))
return render_template('proxadmin/deploy.html', page=page, form=form, product_id=product_id, product_pic=product_pic, product_name=product_name, product_description=product_description, product_recipe=product_recipe)
#COMMAND AND CONTROL
@proxadmin.route("/dashboard", methods=['GET', 'POST'])
@login_required
def dashboard():
#if request.method == 'GET':
deployments = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all()
inv_deployments = []
for invcls in deployments:
if invcls.enabled == True:
inv_deployments.extend([invcls.machine_id])
#if not inv_deployments:
# return render_template('proxadmin/empty_dashboard.html')
rrd = {}
statuses = {}
for cubeid in inv_deployments:
rrd[cubeid] = {}
try:
query = contact_proxmaster({}, 'vmrrd', cubeid)
except:
flash('Deploy #{} unreachable. Support is notified'.format(str(cubeid)))
send_email(current_app.config['MAIL_USERNAME'], 'Cube {} is unreachable'.format(cubeid),
'proxadmin/email/adm_unreachable', user=current_user, cubeid=cubeid)
graphs_list = ['net', 'cpu', 'mem', 'hdd']
try:
for graph in graphs_list:
raw = query[graph]['image'].encode('raw_unicode_escape')
rrd[cubeid][graph] = base64.b64encode(raw).decode()
status = { cubeid : query['status'] }
statuses.update(status)
except Exception as e:
print(e)
flash('Deploy #{} unreachable. Support is notified'.format(str(cubeid)))
send_email(current_app.config['MAIL_USERNAME'], 'Cube {} is unreachable'.format(cubeid),
'proxadmin/email/adm_unreachable', user=current_user, cubeid=cubeid )
contracts = current_user.inv_contracts.order_by(Contract.date_created.desc()).all()
#inv_contracts = []
#for invcls in contracts:
# if invcls.enabled == True:
# inv_contracts.extend([invcls.template])
domains = current_user.inv_domains.order_by(Domain.date_created.desc()).all()
#inv_domains = []
#for invcls in domains:
# if invcls.enabled == True:
# inv_domains.extend([invcls.fqdn])
#print(statuses)
return render_template('proxadmin/dashboard.html', rrd=rrd, status=statuses, inv_deployments=deployments, inv_contracts=contracts, inv_domains=domains)
@proxadmin.route('/vmvnc/<int:vmid>')
@login_required
def vnc(vmid=0):
result = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all()
inventory = []
for invcls in result:
if invcls.enabled == True:
inventory.extend([invcls.machine_id])
#checks if current user owns this vmid
if not vmid in inventory:
print('WARNING: user does not own vmid: ' + str(vmid))
#TODO: log ips
else:
data = {}
db_result = contact_proxmaster(data, 'vmvnc', vmid)
#return render_template('proxadmin/vnc.html', url=db_result['url'])
return redirect(db_result['url'])
abort(404)
valid_commands = ['vmstatus', 'vmstart', 'vmshutdown', 'vmstop']
@proxadmin.route('/<cmd>/<int:vmid>')
@login_required
def command(cmd=None, vmid=0):
#checks whether this is a valid command
if not cmd in valid_commands:
print('WARNING: ' + cmd + ' is not a valid command!')
abort(404)
#if cmd == 'vmstart' and current_user.wallet < 3.0:
# flash('Недостатъчно средства в сметката за тази операция')
# return redirect(url_for('uinvoice.addfunds'))
result = current_user.inv_deployments.order_by(Deployment.date_created.desc()).all()
inventory = []
for invcls in result:
if invcls.enabled == True:
inventory.extend([invcls.machine_id])
#checks if current user owns this vmid
if not vmid in inventory:
print('WARNING: user id:{} does not own cube id:{}'.format(current_user.pid, vmid))
else:
data = {}
db_result = contact_proxmaster(data, cmd, vmid)
#print(db_result)
#TODO: log ips
abort(404)

View file

@ -0,0 +1,277 @@
/*! =======================================================
VERSION 9.4.1
========================================================= */
/*! =========================================================
* bootstrap-slider.js
*
* Maintainers:
* Kyle Kemp
* - Twitter: @seiyria
* - Github: seiyria
* Rohit Kalkur
* - Twitter: @Rovolutionary
* - Github: rovolution
*
* =========================================================
*
* bootstrap-slider is released under the MIT License
* Copyright (c) 2016 Kyle Kemp, Rohit Kalkur, and contributors
*
* 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.
*
* ========================================================= */
.slider {
display: inline-block;
vertical-align: middle;
position: relative;
}
.slider.slider-horizontal {
width: 210px;
height: 20px;
}
.slider.slider-horizontal .slider-track {
height: 10px;
width: 100%;
margin-top: -5px;
top: 50%;
left: 0;
}
.slider.slider-horizontal .slider-selection,
.slider.slider-horizontal .slider-track-low,
.slider.slider-horizontal .slider-track-high {
height: 100%;
top: 0;
bottom: 0;
}
.slider.slider-horizontal .slider-tick,
.slider.slider-horizontal .slider-handle {
margin-left: -10px;
}
.slider.slider-horizontal .slider-tick.triangle,
.slider.slider-horizontal .slider-handle.triangle {
position: relative;
top: 50%;
transform: translateY(-50%);
border-width: 0 10px 10px 10px;
width: 0;
height: 0;
border-bottom-color: #0480be;
margin-top: 0;
}
.slider.slider-horizontal .slider-tick-container {
white-space: nowrap;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.slider.slider-horizontal .slider-tick-label-container {
white-space: nowrap;
margin-top: 20px;
}
.slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
padding-top: 4px;
display: inline-block;
text-align: center;
}
.slider.slider-vertical {
height: 210px;
width: 20px;
}
.slider.slider-vertical .slider-track {
width: 10px;
height: 100%;
left: 25%;
top: 0;
}
.slider.slider-vertical .slider-selection {
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
.slider.slider-vertical .slider-track-low,
.slider.slider-vertical .slider-track-high {
width: 100%;
left: 0;
right: 0;
}
.slider.slider-vertical .slider-tick,
.slider.slider-vertical .slider-handle {
margin-top: -10px;
}
.slider.slider-vertical .slider-tick.triangle,
.slider.slider-vertical .slider-handle.triangle {
border-width: 10px 0 10px 10px;
width: 1px;
height: 1px;
border-left-color: #0480be;
margin-left: 0;
}
.slider.slider-vertical .slider-tick-label-container {
white-space: nowrap;
}
.slider.slider-vertical .slider-tick-label-container .slider-tick-label {
padding-left: 4px;
}
.slider.slider-disabled .slider-handle {
background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
}
.slider.slider-disabled .slider-track {
background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
cursor: not-allowed;
}
.slider input {
display: none;
}
.slider .tooltip.top {
margin-top: -36px;
}
.slider .tooltip-inner {
white-space: nowrap;
max-width: none;
}
.slider .hide {
display: none;
}
.slider-track {
position: absolute;
cursor: pointer;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.slider-selection {
position: absolute;
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-radius: 4px;
}
.slider-selection.tick-slider-selection {
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
}
.slider-track-low,
.slider-track-high {
position: absolute;
background: transparent;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-radius: 4px;
}
.slider-handle {
position: absolute;
top: 0;
width: 20px;
height: 20px;
background-color: #337ab7;
background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
filter: none;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
border: 0px solid transparent;
}
.slider-handle.round {
border-radius: 50%;
}
.slider-handle.triangle {
background: transparent none;
}
.slider-handle.custom {
background: transparent none;
}
.slider-handle.custom::before {
line-height: 20px;
font-size: 20px;
content: '\2605';
color: #726204;
}
.slider-tick {
position: absolute;
width: 20px;
height: 20px;
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
filter: none;
opacity: 0.8;
border: 0px solid transparent;
}
.slider-tick.round {
border-radius: 50%;
}
.slider-tick.triangle {
background: transparent none;
}
.slider-tick.custom {
background: transparent none;
}
.slider-tick.custom::before {
line-height: 20px;
font-size: 20px;
content: '\2605';
color: #726204;
}
.slider-tick.in-selection {
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
opacity: 1;
}

View file

@ -0,0 +1,94 @@
.navbar-default {
background-color: #708d3f;
border-color: #a36123;
position: relative;
}
.navbar-default .navbar-brand {
background: url(/static/images/datapoint.png) center / contain no-repeat;
width: 200px;
color: #ffffff;
}
.navbar-default .navbar-brand:hover,
.navbar-default .navbar-brand:focus {
color: #ffffff;
}
.navbar-default .navbar-text {
color: #ffffff;
}
.navbar-default .navbar-nav > li > a {
color: #ffffff;
}
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
color: #ffffff;
background-color: #a36123;
}
.navbar-default .navbar-nav > li > .dropdown-menu {
background-color: #708d3f;
}
.navbar-default .navbar-nav > li > .dropdown-menu > li > a {
color: #ffffff;
}
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:hover,
.navbar-default .navbar-nav > li > .dropdown-menu > li > a:focus {
color: #ffffff;
background-color: #a36123;
}
.navbar-default .navbar-nav > li > .dropdown-menu > li > .divider {
background-color: #a36123;
}
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #ffffff;
background-color: #a36123;
}
.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
color: #ffffff;
background-color: #a36123;
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .open > a:hover,
.navbar-default .navbar-nav > .open > a:focus {
color: #ffffff;
background-color: #a36123;
}
.navbar-default .navbar-toggle {
border-color: #a36123;
}
.navbar-default .navbar-toggle:hover,
.navbar-default .navbar-toggle:focus {
background-color: #a36123;
}
.navbar-default .navbar-toggle .icon-bar {
background-color: #ffffff;
}
.navbar-default .navbar-collapse,
.navbar-default .navbar-form {
border-color: #ffffff;
}
.navbar-default .navbar-link {
color: #ffffff;
}
.navbar-default .navbar-link:hover {
color: #ffffff;
}
@media (max-width: 767px) {
.navbar-default .navbar-nav .open .dropdown-menu > li > a {
color: #ffffff;
}
.navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
color: #ffffff;
}
.navbar-default .navbar-nav .open .dropdown-menu > .active > a,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #ffffff;
background-color: #a36123;
}
}

View file

@ -0,0 +1,278 @@
/*! nouislider - 9.1.0 - 2016-12-10 16:00:32 */
/* Functional styling;
* These styles are required for noUiSlider to function.
* You don't need to change these rules to apply your design.
*/
.noUi-target,
.noUi-target * {
-webkit-touch-callout: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-user-select: none;
-ms-touch-action: none;
touch-action: none;
-ms-user-select: none;
-moz-user-select: none;
user-select: none;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.noUi-target {
position: relative;
direction: ltr;
}
.noUi-base {
width: 100%;
height: 100%;
position: relative;
z-index: 1; /* Fix 401 */
}
.noUi-connect {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
}
.noUi-origin {
position: absolute;
height: 0;
width: 0;
}
.noUi-handle {
position: relative;
z-index: 1;
}
.noUi-state-tap .noUi-connect,
.noUi-state-tap .noUi-origin {
-webkit-transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
}
.noUi-state-drag * {
cursor: inherit !important;
}
/* Painting and performance;
* Browsers can paint handles in their own layer.
*/
.noUi-base,
.noUi-handle {
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
/* Slider size and handle placement;
*/
.noUi-horizontal {
height: 18px;
}
.noUi-horizontal .noUi-handle {
width: 34px;
height: 28px;
left: -17px;
top: -6px;
}
.noUi-vertical {
width: 18px;
}
.noUi-vertical .noUi-handle {
width: 28px;
height: 34px;
left: -6px;
top: -17px;
}
/* Styling;
*/
.noUi-target {
background: #FAFAFA;
border-radius: 4px;
border: 1px solid #D3D3D3;
box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
}
.noUi-connect {
background: #3FB8AF;
box-shadow: inset 0 0 3px rgba(51,51,51,0.45);
-webkit-transition: background 450ms;
transition: background 450ms;
}
/* Handles and cursors;
*/
.noUi-draggable {
cursor: ew-resize;
}
.noUi-vertical .noUi-draggable {
cursor: ns-resize;
}
.noUi-handle {
border: 1px solid #D9D9D9;
border-radius: 3px;
background: #FFF;
cursor: default;
box-shadow: inset 0 0 1px #FFF,
inset 0 1px 7px #EBEBEB,
0 3px 6px -3px #BBB;
}
.noUi-active {
box-shadow: inset 0 0 1px #FFF,
inset 0 1px 7px #DDD,
0 3px 6px -3px #BBB;
}
/* Handle stripes;
*/
.noUi-handle:before,
.noUi-handle:after {
content: "";
display: block;
position: absolute;
height: 14px;
width: 1px;
background: #E8E7E6;
left: 14px;
top: 6px;
}
.noUi-handle:after {
left: 17px;
}
.noUi-vertical .noUi-handle:before,
.noUi-vertical .noUi-handle:after {
width: 14px;
height: 1px;
left: 6px;
top: 14px;
}
.noUi-vertical .noUi-handle:after {
top: 17px;
}
/* Disabled state;
*/
[disabled] .noUi-connect {
background: #B8B8B8;
}
[disabled].noUi-target,
[disabled].noUi-handle,
[disabled] .noUi-handle {
cursor: not-allowed;
}
/* Base;
*
*/
.noUi-pips,
.noUi-pips * {
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.noUi-pips {
position: absolute;
color: #999;
}
/* Values;
*
*/
.noUi-value {
position: absolute;
text-align: center;
}
.noUi-value-sub {
color: #ccc;
font-size: 10px;
}
/* Markings;
*
*/
.noUi-marker {
position: absolute;
background: #CCC;
}
.noUi-marker-sub {
background: #AAA;
}
.noUi-marker-large {
background: #AAA;
}
/* Horizontal layout;
*
*/
.noUi-pips-horizontal {
padding: 10px 0;
height: 80px;
top: 100%;
left: 0;
width: 100%;
}
.noUi-value-horizontal {
-webkit-transform: translate3d(-50%,50%,0);
transform: translate3d(-50%,50%,0);
}
.noUi-marker-horizontal.noUi-marker {
margin-left: -1px;
width: 2px;
height: 5px;
}
.noUi-marker-horizontal.noUi-marker-sub {
height: 10px;
}
.noUi-marker-horizontal.noUi-marker-large {
height: 15px;
}
/* Vertical layout;
*
*/
.noUi-pips-vertical {
padding: 0 10px;
height: 100%;
top: 0;
left: 100%;
}
.noUi-value-vertical {
-webkit-transform: translate3d(0,50%,0);
transform: translate3d(0,50%,0);
padding-left: 25px;
}
.noUi-marker-vertical.noUi-marker {
width: 5px;
height: 2px;
margin-top: -1px;
}
.noUi-marker-vertical.noUi-marker-sub {
width: 10px;
}
.noUi-marker-vertical.noUi-marker-large {
width: 15px;
}
.noUi-tooltip {
display: block;
position: absolute;
border: 1px solid #D9D9D9;
border-radius: 3px;
background: #fff;
color: #000;
padding: 5px;
text-align: center;
}
.noUi-horizontal .noUi-tooltip {
-webkit-transform: translate(-50%, 0);
transform: translate(-50%, 0);
left: 50%;
bottom: 120%;
}
.noUi-vertical .noUi-tooltip {
-webkit-transform: translate(0, -50%);
transform: translate(0, -50%);
top: 50%;
right: 120%;
}

View file

@ -0,0 +1,100 @@
@import "compass/css3";
$rangeslider: ".rangeslider";
$rangeslider--horizontal: ".rangeslider--horizontal";
$rangeslider--vertical: ".rangeslider--vertical";
$rangeslider--disabled: ".rangeslider--disabled";
$rangeslider--active: ".rangeslider--active";
$rangeslider__fill: ".rangeslider__fill";
$rangeslider__handle: ".rangeslider__handle";
#{$rangeslider},
#{$rangeslider__fill} {
display: block;
@include box-shadow(inset 0px 1px 3px rgba(0,0,0,0.3));
@include border-radius(10px);
}
#{$rangeslider} {
background: #e6e6e6;
position: relative;
}
#{$rangeslider--horizontal} {
height: 20px;
width: 100%;
}
#{$rangeslider--vertical} {
width: 20px;
min-height: 150px;
max-height: 100%;
}
#{$rangeslider--disabled} {
@include opacity(.4);
}
#{$rangeslider__fill} {
background: #00ff00;
position: absolute;
#{$rangeslider--horizontal} & {
top: 0;
height: 100%;
}
#{$rangeslider--vertical} & {
bottom: 0;
width: 100%;
}
}
#{$rangeslider__handle} {
background: white;
border: 1px solid #ccc;
cursor: pointer;
display: inline-block;
width: 40px;
height: 40px;
position: absolute;
@include background-image(linear-gradient(rgba(white, 0), rgba(black, .10)));
@include box-shadow(0 0 8px rgba(black, .3));
@include border-radius(50%);
&:after {
content: "";
display: block;
width: 18px;
height: 18px;
margin: auto;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
@include background-image(linear-gradient(rgba(black, .13), rgba(white, 0)));
@include border-radius(50%);
}
&:active,
#{$rangeslider--active} & {
@include background-image(linear-gradient(rgba(black, .10), rgba(black, .12)));
}
#{$rangeslider--horizontal} & {
top: -10px;
touch-action: pan-y;
-ms-touch-action: pan-y;
}
#{$rangeslider--vertical} & {
left: -10px;
touch-action: pan-x;
-ms-touch-action: pan-x;
}
}
input[type="range"]:focus + #{$rangeslider} #{$rangeslider__handle} {
@include box-shadow(0 0 8px rgba(#ff00ff, .9));
}

View file

@ -0,0 +1,152 @@
body {
/* background: url('/static/images/purplebg.jpg') no-repeat center center fixed; */
background-color: #cccccc;
padding-top: 0px;
-webkit-background-size: cover;
-moz-background-size: cover;
background-size: cover;
-o-background-size: cover;
font-size: 12pt;
font-weight: bold;
}
header {
background: url('/static/images/texture-diagonal.png' repeat, url('static/images/header-layer.jpg') no repeat 50% -25px, #493874 url('/static/images/bg-linear.jpg') repeat-x 50%, -25px;
background-position: 50%, 0;
clear: both;
position: relative;
}
h1 {
color: white;
}
a:link {
color: #2DA6F7;
}
a:visited {
color: #2DA6F7;
}
a:hover {
color: #0099f0;
}
a:active {
color: #2DA6F7;
}
.fluidMedia {
position: relative;
padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
padding-top: 30px;
height: 0;
overflow: hidden;
}
.fluidMedia iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.login-form {
max-width: 350px;
margin-top: 25%;
}
.login-panel {
margin-top: 25%;
}
.form {
max-width: 500px;
}
.padding-left-32 {
padding-left: 32px;
}
.padding-left-16 {
padding-left: 16px;
}
.container-fluid {
position: relative;
max-width: 1170px;
min-width: 480px;
}
.container-fluid-index {
position: relative;
color: #fff;
}
.container-fluid-index a:active {
color: lightgray;
}
.container-fluid-index a:link {
color: lightgray;
}
.container-fluid-index a:hover {
color: #fff;
}
.container-fluid-index a:visited {
color: lightgray;
}
.roundavatar {
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
}
.btn-outline {
color: inherit;
background-color: transparent;
}
.btn-primary.btn-outline {
color: #428bca;
}
.btn-success.btn-outline {
color: #5cb85c;
}
.btn-info.btn-outline {
color: #5bc0de;
}
.btn-warning.btn-outline {
color: #f0ad4e;
}
.btn-danger.btn-outline {
color: #d9534f;
}
.btn-primary.btn-outline:hover,
.btn-success.btn-outline:hover,
.btn-info.btn-outline:hover,
.btn-warning.btn-outline:hover,
.btn-danger.btn-outline:hover {
color: #fff;
}
.btn-primary.btn-outline:focus,
.btn-success.btn-outline:focus,
.btn-info.btn-outline:focus,
.btn-warning.btn-outline:focus,
.btn-danger.btn-outline:focus {
background-color: #fff;
border-color: #070;
}

View file

@ -0,0 +1,121 @@
body {
background: url('/static/images/bg.jpg') no-repeat center center fixed;
background: #000000;
background-color: #f8f8f8;
padding-top: 0px;
-webkit-background-size: cover;
-moz-background-size: cover;
background-size: cover;
-o-background-size: cover;
color: gray;
}
body {
color: gray;
font-size: 12pt;
font-weight: bold;
}
h1 {
color: white;
}
.fluidMedia {
position: relative;
padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
padding-top: 30px;
height: 0;
overflow: hidden;
}
.fluidMedia iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.login-form {
max-width: 350px;
margin-top: 25%;
}
.login-panel {
margin-top: 25%;
}
.form {
max-width: 500px;
}
.padding-left-32 {
padding-left: 32px;
}
.padding-left-16 {
padding-left: 16px;
}
.container-fluid {
position: relative;
max-width: 1170px;
min-width: 480px;
}
.roundavatar {
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
}
.navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus {
color: #fff;
border-color: #070;
background-color: #2b7845;
}
.btn-outline {
color: inherit;
background-color: transparent;
}
.btn-primary.btn-outline {
color: #428bca;
}
.btn-success.btn-outline {
color: #5cb85c;
}
.btn-info.btn-outline {
color: #5bc0de;
}
.btn-warning.btn-outline {
color: #f0ad4e;
}
.btn-danger.btn-outline {
color: #d9534f;
}
.btn-primary.btn-outline:hover,
.btn-success.btn-outline:hover,
.btn-info.btn-outline:hover,
.btn-warning.btn-outline:hover,
.btn-danger.btn-outline:hover {
color: #fff;
}
.btn-primary.btn-outline:focus,
.btn-success.btn-outline:focus,
.btn-info.btn-outline:focus,
.btn-warning.btn-outline:focus,
.btn-danger.btn-outline:focus {
background-color: #fff;
border-color: #070;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

1807
proxadmin/static/js/bootstrap-slider.js vendored Normal file

File diff suppressed because it is too large Load diff

4
proxadmin/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

1850
proxadmin/static/js/mgui.js Normal file

File diff suppressed because it is too large Load diff

3
proxadmin/static/js/nouislider.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,493 @@
(function(factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function($) {
'use strict';
// Polyfill Number.isNaN(value)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN
Number.isNaN = Number.isNaN || function(value) {
return typeof value === 'number' && value !== value;
};
/**
* Range feature detection
* @return {Boolean}
*/
function supportsRange() {
var input = document.createElement('input');
input.setAttribute('type', 'range');
return input.type !== 'text';
}
var pluginName = 'rangeslider',
pluginIdentifier = 0,
hasInputRangeSupport = supportsRange(),
defaults = {
polyfill: true,
orientation: 'horizontal',
rangeClass: 'rangeslider',
disabledClass: 'rangeslider--disabled',
activeClass: 'rangeslider--active',
horizontalClass: 'rangeslider--horizontal',
verticalClass: 'rangeslider--vertical',
fillClass: 'rangeslider__fill',
handleClass: 'rangeslider__handle',
startEvent: ['mousedown', 'touchstart', 'pointerdown'],
moveEvent: ['mousemove', 'touchmove', 'pointermove'],
endEvent: ['mouseup', 'touchend', 'pointerup']
},
constants = {
orientation: {
horizontal: {
dimension: 'width',
direction: 'left',
directionStyle: 'left',
coordinate: 'x'
},
vertical: {
dimension: 'height',
direction: 'top',
directionStyle: 'bottom',
coordinate: 'y'
}
}
};
/**
* Delays a function for the given number of milliseconds, and then calls
* it with the arguments supplied.
*
* @param {Function} fn [description]
* @param {Number} wait [description]
* @return {Function}
*/
function delay(fn, wait) {
var args = Array.prototype.slice.call(arguments, 2);
return setTimeout(function(){ return fn.apply(null, args); }, wait);
}
/**
* Returns a debounced function that will make sure the given
* function is not triggered too much.
*
* @param {Function} fn Function to debounce.
* @param {Number} debounceDuration OPTIONAL. The amount of time in milliseconds for which we will debounce the function. (defaults to 100ms)
* @return {Function}
*/
function debounce(fn, debounceDuration) {
debounceDuration = debounceDuration || 100;
return function() {
if (!fn.debouncing) {
var args = Array.prototype.slice.apply(arguments);
fn.lastReturnVal = fn.apply(window, args);
fn.debouncing = true;
}
clearTimeout(fn.debounceTimeout);
fn.debounceTimeout = setTimeout(function(){
fn.debouncing = false;
}, debounceDuration);
return fn.lastReturnVal;
};
}
/**
* Check if a `element` is visible in the DOM
*
* @param {Element} element
* @return {Boolean}
*/
function isHidden(element) {
return (
element && (
element.offsetWidth === 0 ||
element.offsetHeight === 0 ||
// Also Consider native `<details>` elements.
element.open === false
)
);
}
/**
* Get hidden parentNodes of an `element`
*
* @param {Element} element
* @return {[type]}
*/
function getHiddenParentNodes(element) {
var parents = [],
node = element.parentNode;
while (isHidden(node)) {
parents.push(node);
node = node.parentNode;
}
return parents;
}
/**
* Returns dimensions for an element even if it is not visible in the DOM.
*
* @param {Element} element
* @param {String} key (e.g. offsetWidth )
* @return {Number}
*/
function getDimension(element, key) {
var hiddenParentNodes = getHiddenParentNodes(element),
hiddenParentNodesLength = hiddenParentNodes.length,
inlineStyle = [],
dimension = element[key];
// Used for native `<details>` elements
function toggleOpenProperty(element) {
if (typeof element.open !== 'undefined') {
element.open = (element.open) ? false : true;
}
}
if (hiddenParentNodesLength) {
for (var i = 0; i < hiddenParentNodesLength; i++) {
// Cache style attribute to restore it later.
inlineStyle[i] = hiddenParentNodes[i].style.cssText;
// visually hide
if (hiddenParentNodes[i].style.setProperty) {
hiddenParentNodes[i].style.setProperty('display', 'block', 'important');
} else {
hiddenParentNodes[i].style.cssText += ';display: block !important';
}
hiddenParentNodes[i].style.height = '0';
hiddenParentNodes[i].style.overflow = 'hidden';
hiddenParentNodes[i].style.visibility = 'hidden';
toggleOpenProperty(hiddenParentNodes[i]);
}
// Update dimension
dimension = element[key];
for (var j = 0; j < hiddenParentNodesLength; j++) {
// Restore the style attribute
hiddenParentNodes[j].style.cssText = inlineStyle[j];
toggleOpenProperty(hiddenParentNodes[j]);
}
}
return dimension;
}
/**
* Returns the parsed float or the default if it failed.
*
* @param {String} str
* @param {Number} defaultValue
* @return {Number}
*/
function tryParseFloat(str, defaultValue) {
var value = parseFloat(str);
return Number.isNaN(value) ? defaultValue : value;
}
/**
* Capitalize the first letter of string
*
* @param {String} str
* @return {String}
*/
function ucfirst(str) {
return str.charAt(0).toUpperCase() + str.substr(1);
}
/**
* Plugin
* @param {String} element
* @param {Object} options
*/
function Plugin(element, options) {
this.$window = $(window);
this.$document = $(document);
this.$element = $(element);
this.options = $.extend( {}, defaults, options );
this.polyfill = this.options.polyfill;
this.orientation = this.$element[0].getAttribute('data-orientation') || this.options.orientation;
this.onInit = this.options.onInit;
this.onSlide = this.options.onSlide;
this.onSlideEnd = this.options.onSlideEnd;
this.DIMENSION = constants.orientation[this.orientation].dimension;
this.DIRECTION = constants.orientation[this.orientation].direction;
this.DIRECTION_STYLE = constants.orientation[this.orientation].directionStyle;
this.COORDINATE = constants.orientation[this.orientation].coordinate;
// Plugin should only be used as a polyfill
if (this.polyfill) {
// Input range support?
if (hasInputRangeSupport) { return false; }
}
this.identifier = 'js-' + pluginName + '-' +(pluginIdentifier++);
this.startEvent = this.options.startEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
this.moveEvent = this.options.moveEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
this.endEvent = this.options.endEvent.join('.' + this.identifier + ' ') + '.' + this.identifier;
this.toFixed = (this.step + '').replace('.', '').length - 1;
this.$fill = $('<div class="' + this.options.fillClass + '" />');
this.$handle = $('<div class="' + this.options.handleClass + '" />');
this.$range = $('<div class="' + this.options.rangeClass + ' ' + this.options[this.orientation + 'Class'] + '" id="' + this.identifier + '" />').insertAfter(this.$element).prepend(this.$fill, this.$handle);
// visually hide the input
this.$element.css({
'position': 'absolute',
'width': '1px',
'height': '1px',
'overflow': 'hidden',
'opacity': '0'
});
// Store context
this.handleDown = $.proxy(this.handleDown, this);
this.handleMove = $.proxy(this.handleMove, this);
this.handleEnd = $.proxy(this.handleEnd, this);
this.init();
// Attach Events
var _this = this;
this.$window.on('resize.' + this.identifier, debounce(function() {
// Simulate resizeEnd event.
delay(function() { _this.update(false, false); }, 300);
}, 20));
this.$document.on(this.startEvent, '#' + this.identifier + ':not(.' + this.options.disabledClass + ')', this.handleDown);
// Listen to programmatic value changes
this.$element.on('change.' + this.identifier, function(e, data) {
if (data && data.origin === _this.identifier) {
return;
}
var value = e.target.value,
pos = _this.getPositionFromValue(value);
_this.setPosition(pos);
});
}
Plugin.prototype.init = function() {
this.update(true, false);
if (this.onInit && typeof this.onInit === 'function') {
this.onInit();
}
};
Plugin.prototype.update = function(updateAttributes, triggerSlide) {
updateAttributes = updateAttributes || false;
if (updateAttributes) {
this.min = tryParseFloat(this.$element[0].getAttribute('min'), 0);
this.max = tryParseFloat(this.$element[0].getAttribute('max'), 100);
this.value = tryParseFloat(this.$element[0].value, Math.round(this.min + (this.max-this.min)/2));
this.step = tryParseFloat(this.$element[0].getAttribute('step'), 1);
}
this.handleDimension = getDimension(this.$handle[0], 'offset' + ucfirst(this.DIMENSION));
this.rangeDimension = getDimension(this.$range[0], 'offset' + ucfirst(this.DIMENSION));
this.maxHandlePos = this.rangeDimension - this.handleDimension;
this.grabPos = this.handleDimension / 2;
this.position = this.getPositionFromValue(this.value);
// Consider disabled state
if (this.$element[0].disabled) {
this.$range.addClass(this.options.disabledClass);
} else {
this.$range.removeClass(this.options.disabledClass);
}
this.setPosition(this.position, triggerSlide);
};
Plugin.prototype.handleDown = function(e) {
e.preventDefault();
this.$document.on(this.moveEvent, this.handleMove);
this.$document.on(this.endEvent, this.handleEnd);
// add active class because Firefox is ignoring
// the handle:active pseudo selector because of `e.preventDefault();`
this.$range.addClass(this.options.activeClass);
// If we click on the handle don't set the new position
if ((' ' + e.target.className + ' ').replace(/[\n\t]/g, ' ').indexOf(this.options.handleClass) > -1) {
return;
}
var pos = this.getRelativePosition(e),
rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
handlePos = this.getPositionFromNode(this.$handle[0]) - rangePos,
setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
this.setPosition(setPos);
if (pos >= handlePos && pos < handlePos + this.handleDimension) {
this.grabPos = pos - handlePos;
}
};
Plugin.prototype.handleMove = function(e) {
e.preventDefault();
var pos = this.getRelativePosition(e);
var setPos = (this.orientation === 'vertical') ? (this.maxHandlePos - (pos - this.grabPos)) : (pos - this.grabPos);
this.setPosition(setPos);
};
Plugin.prototype.handleEnd = function(e) {
e.preventDefault();
this.$document.off(this.moveEvent, this.handleMove);
this.$document.off(this.endEvent, this.handleEnd);
this.$range.removeClass(this.options.activeClass);
// Ok we're done fire the change event
this.$element.trigger('change', { origin: this.identifier });
if (this.onSlideEnd && typeof this.onSlideEnd === 'function') {
this.onSlideEnd(this.position, this.value);
}
};
Plugin.prototype.cap = function(pos, min, max) {
if (pos < min) { return min; }
if (pos > max) { return max; }
return pos;
};
Plugin.prototype.setPosition = function(pos, triggerSlide) {
var value, newPos;
if (triggerSlide === undefined) {
triggerSlide = true;
}
// Snapping steps
value = this.getValueFromPosition(this.cap(pos, 0, this.maxHandlePos));
newPos = this.getPositionFromValue(value);
// Update ui
this.$fill[0].style[this.DIMENSION] = (newPos + this.grabPos) + 'px';
this.$handle[0].style[this.DIRECTION_STYLE] = newPos + 'px';
this.setValue(value);
// Update globals
this.position = newPos;
this.value = value;
if (triggerSlide && this.onSlide && typeof this.onSlide === 'function') {
this.onSlide(newPos, value);
}
};
// Returns element position relative to the parent
Plugin.prototype.getPositionFromNode = function(node) {
var i = 0;
while (node !== null) {
i += node.offsetLeft;
node = node.offsetParent;
}
return i;
};
Plugin.prototype.getRelativePosition = function(e) {
// Get the offset DIRECTION relative to the viewport
var ucCoordinate = ucfirst(this.COORDINATE),
rangePos = this.$range[0].getBoundingClientRect()[this.DIRECTION],
pageCoordinate = 0;
if (typeof e.originalEvent['client' + ucCoordinate] !== 'undefined') {
pageCoordinate = e.originalEvent['client' + ucCoordinate];
}
else if (
e.originalEvent.touches &&
e.originalEvent.touches[0] &&
typeof e.originalEvent.touches[0]['client' + ucCoordinate] !== 'undefined'
) {
pageCoordinate = e.originalEvent.touches[0]['client' + ucCoordinate];
}
else if(e.currentPoint && typeof e.currentPoint[this.COORDINATE] !== 'undefined') {
pageCoordinate = e.currentPoint[this.COORDINATE];
}
return pageCoordinate - rangePos;
};
Plugin.prototype.getPositionFromValue = function(value) {
var percentage, pos;
percentage = (value - this.min)/(this.max - this.min);
pos = (!Number.isNaN(percentage)) ? percentage * this.maxHandlePos : 0;
return pos;
};
Plugin.prototype.getValueFromPosition = function(pos) {
var percentage, value;
percentage = ((pos) / (this.maxHandlePos || 1));
value = this.step * Math.round(percentage * (this.max - this.min) / this.step) + this.min;
return Number((value).toFixed(this.toFixed));
};
Plugin.prototype.setValue = function(value) {
if (value === this.value && this.$element[0].value !== '') {
return;
}
// Set the new value and fire the `input` event
this.$element
.val(value)
.trigger('input', { origin: this.identifier });
};
Plugin.prototype.destroy = function() {
this.$document.off('.' + this.identifier);
this.$window.off('.' + this.identifier);
this.$element
.off('.' + this.identifier)
.removeAttr('style')
.removeData('plugin_' + pluginName);
// Remove the generated markup
if (this.$range && this.$range.length) {
this.$range[0].parentNode.removeChild(this.$range[0]);
}
};
// A really lightweight plugin wrapper around the constructor,
// preventing against multiple instantiations
$.fn[pluginName] = function(options) {
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var $this = $(this),
data = $this.data('plugin_' + pluginName);
// Create a new instance.
if (!data) {
$this.data('plugin_' + pluginName, (data = new Plugin(this, options)));
}
// Make it possible to access methods from public.
// e.g `$element.rangeslider('method');`
if (typeof options === 'string') {
data[options].apply(data, args);
}
});
};
return 'rangeslider.js is available in jQuery context e.g $(selector).rangeslider(options);';
}));

View file

@ -0,0 +1,12 @@
{% macro render_field(field) %}
<dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</dd>
{% endmacro %}

View file

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block page_content %}
<h1>Index</h1>
<div id="myCarousel" class="carousel slide" data-ride="carousel">
<!-- Indicators -->
<ol class="carousel-indicators">
<li data-target="#myCarousel" data-slide-to="0" class="active"></li>
<li data-target="#myCarousel" data-slide-to="1"></li>
<li data-target="#myCarousel" data-slide-to="2"></li>
<li data-target="#myCarousel" data-slide-to="3"></li>
</ol>
<!-- Wrapper for slides -->
<div class="carousel-inner" role="listbox">
<div class="item active">
<img src="static/images/1.jpg" alt="Chania">
</div>
<div class="item">
<img src="static/images/2.jpg" alt="Chania">
</div>
<div class="item">
<img src="static/images/3.jpg" alt="Flower">
</div>
<div class="item">
<img src="static/images/4.jpg" alt="Flower">
</div>
</div>
<!-- Left and right controls -->
<a class="left carousel-control" href="#myCarousel" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#myCarousel" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
<div class="row">
<div class="col-md-4">
<p>Yr umami selfies Carles DIY, pop-up Tonx meggings stumptown freegan street art Vice ethnic. Pickled gastropub lo-fi polaroid, ennui selvage meh Tumblr organic iPhone kale chips narwhal Echo Park. Tonx literally distillery Pitchfork McSweeney's semiotics. Stumptown YOLO fanny pack bespoke, kitsch Carles gastropub vegan. Biodiesel ennui church-key McSweeney's, selvage hoodie Brooklyn 90's lomo. Quinoa photo booth cliche semiotics. Roof party Etsy ethnic, fashion axe mlkshk 8-bit paleo.</p>
</div>
<div class="col-md-4">
<p>Yr umami selfies Carles DIY, pop-up Tonx meggings stumptown freegan street art Vice ethnic. Pickled gastropub lo-fi polaroid, ennui selvage meh Tumblr organic iPhone kale chips narwhal Echo Park. Tonx literally distillery Pitchfork McSweeney's semiotics. Stumptown YOLO fanny pack bespoke, kitsch Carles gastropub vegan. Biodiesel ennui church-key McSweeney's, selvage hoodie Brooklyn 90's lomo. Quinoa photo booth cliche semiotics. Roof party Etsy ethnic, fashion axe mlkshk 8-bit paleo.</p>
</div>
<div class="col-md-4">
<p>Yr umami selfies Carles DIY, pop-up Tonx meggings stumptown freegan street art Vice ethnic. Pickled gastropub lo-fi polaroid, ennui selvage meh Tumblr organic iPhone kale chips narwhal Echo Park. Tonx literally distillery Pitchfork McSweeney's semiotics. Stumptown YOLO fanny pack bespoke, kitsch Carles gastropub vegan. Biodiesel ennui church-key McSweeney's, selvage hoodie Brooklyn 90's lomo. Quinoa photo booth cliche semiotics. Roof party Etsy ethnic, fashion axe mlkshk 8-bit paleo.</p>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}2FA{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Вашият акаунт е вече потвърден.{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
Здравейте, {{ current_user.email }}!
</h1>
<h3>Вашият акаунт е вече потвърден.</h3>
<p>
Моля напуснете тази страница :)
</p>
<p>
<a href="{{ url_for('proxadmin.index') }}">Натиснете тук за изход</a>
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Change Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}

View file

@ -0,0 +1,6 @@
<p>{{ user.email }} logged in.<br />
<br />
IP Address: {{ ipaddr }}<br />
</p>
<p>Regards,
Proxadmin</p>

View file

@ -0,0 +1,6 @@
User {{ user.email }} logged in.
IP Address: {{ ipaddr }}
Regards,
Proxadmin

View file

@ -0,0 +1,6 @@
New user {{ user.email }} has been registered.
IP Address: {{ ipaddr }}
Regards,
Proxadmin

View file

@ -0,0 +1,6 @@
New user {{ user.email }} has been registered.
IP Address: {{ ipaddr }}
Regards,
Proxadmin

View file

@ -0,0 +1,8 @@
<p>Dear {{ user.name }},</p>
<p>Welcome to <b>Proxadmin</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>Datapoint.bg</p>
<p><small>Note: replies to this email address are not monitored.</small></p>

View file

@ -0,0 +1,13 @@
Dear {{ user.name }},
Welcome to Datapoint!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
Datapoint.bg
Note: replies to this email address are not monitored.

View file

@ -0,0 +1,5 @@
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p><small>Note: replies to this email address are not monitored.</small></p>

View file

@ -0,0 +1,9 @@
Dear {{ user.username }},
To reset your password click on the following link:
{{ url_for('auth.password_reset', token=token, _external=True) }}
If you have not requested a password reset simply ignore this message.
Note: replies to this email address are not monitored.

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}

View file

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Register{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Register</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
</div>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Password Reset{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Confirm your account{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
Hello, {{ current_user.username }}!
</h1>
<h3>You have not confirmed your account yet.</h3>
<p>
Before you can access this site you need to confirm your account.
Check your inbox, you should have received an email with a confirmation link.
</p>
<p>
Need another confirmation email?
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "bootstrap/fixes.html" as fixes %}
{% import "bootstrap/utils.html" as util %}
{% block title %}Panel{% endblock %}
{% block html_attribs %} lang="en"{% endblock %}
{% block styles %}
{{ super() }}
<link href="{{ url_for('static', filename='css/navbar.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/nouislider.css') }}" rel="stylesheet">
{% endblock %}
{% block scripts %}
{{ super() }}
<script type=text/javascript src="{{ url_for('static', filename='js/jquery.js') }}"></script>
<script type=text/javascript src="{{ url_for('static', filename='js/nouislider.min.js') }}"></script>
{% endblock %}
{% block navbar %}
{% include "nav.html" %}
{% endblock %}
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">x</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
{% endblock %}
{% block footer %}
{% include "footer.html" %}
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Forbidden{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Forbidden</h1>
</div>
<pre>
ooo, .---.
o` o / |\________________
o` 'oooo() | ________ _ _)
`oo o` \ |/ | | | |
`ooo' `---' "-" |_|
</pre>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
</div>
<pre>
\ SORRY /
\ /
\ This page does /
] not exist yet. [ ,'|
] [ / |
]___ ___[ ,' |
] ]\ /[ [ |: |
] ] \ / [ [ |: |
] ] ] [ [ [ |: |
] ] ]__ __[ [ [ |: |
] ] ] ]\ _ /[ [ [ [ |: |
] ] ] ] (#) [ [ [ [ :===='
] ] ]_].nHn.[_[ [ [
] ] ] HHHHH. [ [ [
] ] / `HH("N \ [ [
]__]/ HHH " \[__[
] NNN [
] N/" [
] N H [
/ N \
/ q, \
/ \
</pre>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Internal Server Error{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Internal Server Error</h1>
</div>
<pre>
.--. .--.
_ ` \ / ` _
`\.===. \.^./ .===./`
\/`"`\/
, | 500 | ,
/ `\|;-.-'|/` \
/ |::\ | \
.-' ,-'`|:::; |`'-, '-.
| |::::\| |
| |::::;| |
| \::::// |
| `.://' |
.' `.
_,' `,_
</pre>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}CSRF Error{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>{{ reason }}</h1>
</div>
<pre>
ooo, .---.
o` o / |\________________
o` 'oooo() | ________ _ _)
`oo o` \ |/ | | | |
`ooo' `---' "-" |_|
</pre>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% block page_footer %}
<br />
<hr>
<div id="footer">
<p>
&copy; 2017 <a href="https://panel.datapoint.bg/aboutus">datapoint.bg</a>
</p>
</div>
{% endblock %}

View file

@ -0,0 +1,70 @@
{% block navbar %}
<div class="container-fluid navbar-inverse navbar-fixed-top">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% if not current_user.is_authenticated %}
<a class="navbar-brand" href="{{ url_for('proxadmin.index') }}" rel="home"><span><img style="max-width:100px; margin-top: -7px;" src="../static/images/hex24.png"></span> proxmaster</a>
{% else %}
<a class="navbar-brand" href="{{ url_for('proxadmin.index') }}" rel="home"><span><img style="max-width:100px; margin-top: -7px;" src="../static/images/hex24.png"></span> proxmaster</a>
{% endif %}
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
{% if current_user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-togle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Solution Market<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('proxadmin.market') }}">Applications</a></li>
<li><a href="{{ url_for('proxadmin.market') }}">Schemas</a></li>
</ul>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Community<span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('proxadmin.dashboard') }}">Groups</a></li>
<li><a href="{{ url_for('proxadmin.dashboard') }}">Blog</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">r5</a></li>
</ul>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if not current_user.is_authenticated %}
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% else %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><img class="avatar" src="{{ current_user.gravatar(20) }}"> {{ current_user.email }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('proxadmin.dashboard') }}"><span class="glyphicon glyphicon-eye-open"></span> Dashboard</a></li>
<li><a href="{{ url_for('proxadmin.profile') }}"><span class="glyphicon glyphicon-flag"></span> Policy</a></li>
<li><a href="{{ url_for('proxadmin.profile') }}"><span class="glyphicon glyphicon-list"></span> Drafts</a></li>
<li role="separator" class="divider"></li>
<li><a href="{{ url_for('proxadmin.profile') }}"><span class="glyphicon glyphicon-user"></span> Profile</a></li>
<li><a href="{{ url_for('proxadmin.profile') }}"><span class="glyphicon glyphicon-question-sign"></span> Help</a></li>
<li><a href="{{ url_for('proxadmin.profile') }}"><span class="glyphicon glyphicon-cog"></span> Settings</a></li>
<li><a href="{{ url_for('auth.logout') }}"><span class="glyphicon glyphicon-off"></span> Logout</a></li>
</ul>
</li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
{% endblock %}

View file

@ -0,0 +1,60 @@
{% block navbar %}
<div class="container-fluid navbar-default navbar-fixed-top">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% if not current_user.is_authenticated %}
<a class="navbar-brand" href="{{ url_for('proxadmin.index') }}" rel="home"></a>
{% else %}
<a class="navbar-brand" href="{{ url_for('proxadmin.dashboard') }}" rel="home"></a>
{% endif %}
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
{% if current_user.is_authenticated %}
<li><a href="/deploy/7"><span class="glyphicon glyphicon-send"></span> Deploy Application</a></li>
{% else %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><span class="glyphicon glyphicon-send"></span> Deploy Application</a>
<ul class="dropdown-menu">
<li><a href="/market/1"><span class="glyphicon glyphicon-send"></span> Business</a></li>
<li><a href="/market/2"><span class="glyphicon glyphicon-send"></span> Games</a></li>
<li><a href="/market/3"><span class="glyphicon glyphicon-send"></span> Misc</a></li>
</ul>
{% endif %}
<li><a href="{{ url_for('proxadmin.chat') }}" target="_blank"><span class="glyphicon glyphicon-question-sign"></span> Live Chat</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if not current_user.is_authenticated %}
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
{% else %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"><img class="avatar" src="{{ current_user.gravatar(20) }}"> {{ current_user.email }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('proxadmin.dashboard') }}"><span class="glyphicon glyphicon-eye-open"></span> Dashboard</a></li>
<li><a href="{{ url_for('uinvoice.documents') }}"><span class="glyphicon glyphicon-list-alt"></span> Orders</a></li>
<li role="separator" class="divider"></li>
<li><a href="{{ url_for('uinvoice.profile') }}"><span class="glyphicon glyphicon-user"></span> Profile</a></li>
<li><a href="{{ url_for('auth.logout') }}"><span class="glyphicon glyphicon-off"></span> Logout</a></li>
</ul>
</li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
{% endblock %}

View file

@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block page_content %}
<script>
// Only run what comes next *after* the page has loaded
addEventListener("DOMContentLoaded", function() {
// Grab all of the elements with a class of command
// (which all of the buttons we just created have)
var commandButtons = document.querySelectorAll(".command");
for (var i=0, l=commandButtons.length; i<l; i++) {
var button = commandButtons[i];
// For each button, listen for the "click" event
button.addEventListener("click", function(e) {
// When a click happens, stop the button
// from submitting our form (if we have one)
e.preventDefault();
var clickedButton = e.target;
var command = clickedButton.value;
var vmid = clickedButton.getAttribute('vmid');
// Now we need to send the data to our server
// without reloading the page - this is the domain of
// AJAX (Asynchronous JavaScript And XML)
// We will create a new request object
// and set up a handler for the response
var request = new XMLHttpRequest();
request.onload = function() {
// We could do more interesting things with the response
// or, we could ignore it entirely
//alert(request.responseText);
};
// We point the request at the appropriate command
request.open("GET", "/" + command + "/" + vmid, true);
// and then we send it off
request.send();
});
}
}, true);
</script>
<div class="container-fluid">
<br />
<div class="row">
<div class="col-md">
<div class="panel-group">
{% for key, value in current_user.inventory.items() %}
<div class="panel panel-default">
<div class="panel-heading">{{ value['hostname'] }} ({{ key }})</div>
<div class="panel-body"><p>
<button class="btn btn-default btn-info" onclick="window.open('/vmvnc/{{ value['vmid'] }}','popUpWindow','height=768,width=1280,left=2,top=2,,scrollbars=no,menubar=no'); return false;"><span class="glyphicon glyphicon-console" aria-hidden="true"></span> Console</button>
<button class="command command-vmstart btn btn-default btn-success" value="vmstart" vmid="{{ value['vmid'] }}"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Start</button>
<button class="command command-vmshutdown btn btn-default btn-warning" value="vmshutdown" vmid="{{ value['vmid'] }}"><span class="glyphicon glyphicon-off" aria-hidden="true"></span> Shutdown</button>
<button class="command command-vmstop btn btn-default btn-danger" value="vmstop" vmid="{{ value['vmid'] }}"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> Force Stop</button>
</p>
<table>
<p>
<tr>
<td><img width=540 src="data:image/png;base64,{{ rrd[value['vmid']]['net'] }}"></td>
<td><img width=540 src="data:image/png;base64,{{ rrd[value['vmid']]['cpu'] }}"></td>
</tr>
<tr>
<th>Network</th>
<th>CPU</th>
</tr>
</p>
</table>
<table>
<p>
<tr>
<td><img width=540 src="data:image/png;base64,{{ rrd[value['vmid']]['mem'] }}"></td>
<td><img width=540 src="data:image/png;base64,{{ rrd[value['vmid']]['hdd'] }}"></td>
</tr>
<tr>
<th>Memory</th>
<th>Hard Disk</th>
</tr>
</p>
</table>
</p>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="row">
<div class="col-md">
<div class="panel panel-default">
<div class="panel-heading">
<i class="fa fa-bell fa-fw"></i> Notifications Panel
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<div class="list-group">
<a href="#" class="list-group-item">
<i class="fa fa-comment fa-fw"></i> New Comment
<span class="pull-right text-muted small"><em>4 minutes ago</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-twitter fa-fw"></i> 3 New Followers
<span class="pull-right text-muted small"><em>12 minutes ago</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-envelope fa-fw"></i> Message Sent
<span class="pull-right text-muted small"><em>27 minutes ago</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-tasks fa-fw"></i> New Task
<span class="pull-right text-muted small"><em>43 minutes ago</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-upload fa-fw"></i> Server Rebooted
<span class="pull-right text-muted small"><em>11:32 AM</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-bolt fa-fw"></i> Server Crashed!
<span class="pull-right text-muted small"><em>11:13 AM</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-warning fa-fw"></i> Server Not Responding
<span class="pull-right text-muted small"><em>10:57 AM</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-shopping-cart fa-fw"></i> New Order Placed
<span class="pull-right text-muted small"><em>9:49 AM</em>
</span>
</a>
<a href="#" class="list-group-item">
<i class="fa fa-money fa-fw"></i> Payment Received
<span class="pull-right text-muted small"><em>Yesterday</em>
</span>
</a>
</div>
<!-- /.list-group -->
<a href="#" class="btn btn-default btn-block">View All Alerts</a>
</div>
<!-- /.panel-body -->
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block page_content %}
<h1>About us</h1>
<center>
<div class="row">
<img src="../../static/images/datapoint.png" class="img-responsive"></img>
</div>
<p style="text-align: left;"> datapoint.bg е проект на Новахостинг ЕООД който създадохме, за да предоставим на потребителите ново поколение изчислителни услуги. Стремим се да помагаме на неуверените и неопитни потребители в първата им среща с тях, като сме на линия за съвети отновно одимите услуги и инструменти, за да не влагате излишни средства в ресурси.</p>
<h3 style="text-align: left;"> Историята</h3>
<p style="text-align: left;"> Този сайт е естествено продължение на няколко лични проекта на двама ентусиасти, на едно пространство, използвано по много предназначения и наричано от нас просто “избата“. Там е мястото, където се затваряхме, за да свършим малко работа бързо и без никой да знае къде сме. От там са тръгнали много идеи, за да оживеят в сървърното помещение, което изградихме първоначално за собствена употреба. После пораснахме и дойдоха идеите, а след тях и клиентите ни. Както споменах естественото продължение на нещата беше да затворим цялостно процеса на работа и усъвършенствахме сървърното помещение, за да е пригодно за крайни клиенти. Докато се усетим имахме готово чисто ново сървърно помещение- мечта за всеки системен администратор. С мощни сървъри, гъвкава мрежова инфраструктура, резервирана свързаност с най- големите български интернет доставчици, резервирано захранване, климатични системи, температурен контрол, цялостен мониторинг и ново поколение охранителна система.</p>
<h3 style="text-align: left;">Мястото</h3>
<p style="text-align: left;"> За да сме мотивирани, не просто работим. Вършим задачите си с удоволствие в най- приятния офис &#8211; малкият двор на “избата“. Това място за нас е свещенно &#8211; то ни зарежда и мотивира, там си почиваме, провеждаме работните си срещи и обсъждаме работата си.</p>
<h3 style="text-align: left;">Екипът</h3>
<p>Като модерна и устремена към успех компания, в нашата структура няма строго определено йерархично ниво. Вскички сме ХОРА, равни пред идеите си.</p>
</div>
{% endblock %}

View file

@ -0,0 +1,188 @@
{% extends "base.html" %}
{% block page_content %}
<script>
// Only run what comes next *after* the page has loaded
addEventListener("DOMContentLoaded", function() {
// Grab all of the elements with a class of command
// (which all of the buttons we just created have)
var commandButtons = document.querySelectorAll(".command");
for (var i=0, l=commandButtons.length; i<l; i++) {
var button = commandButtons[i];
// For each button, listen for the "click" event
button.addEventListener("click", function(e) {
// When a click happens, stop the button
// from submitting our form (if we have one)
e.preventDefault();
var clickedButton = e.target;
var command = clickedButton.value;
var vmid = clickedButton.getAttribute('vmid');
// Now we need to send the data to our server
// without reloading the page - this is the domain of
// AJAX (Asynchronous JavaScript And XML)
// We will create a new request object
// and set up a handler for the response
var request = new XMLHttpRequest();
request.onload = function() {
// We could do more interesting things with the response
// or, we could ignore it entirely
//alert(request.responseText);
};
// We point the request at the appropriate command
request.open("GET", "/" + command + "/" + vmid, true);
// and then we send it off
request.send();
});
}
}, true);
</script>
<style type="text/css">
.tg {border-collapse:collapse;border-spacing:0;}
.tg td{font-family:Arial, sans-serif;font-size:14px;padding:1px 1px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
.tg th{font-family:Arial, sans-serif;font-size:14px;padding:1px 1px;font-weight:normal;padding:border-style:solid;border-width:1px;overflow:hidden;word-break:normal;}
.tg .tg-yw4l{vertical-align:top}
</style>
<div class="container-fluid">
<br />
<div class="row">
{% block sidebar %}
{% include "uinvoice/_sidebar.html" %}
{% endblock %}
<div class="col-md-4">
<div class="panel panel-default" id="contracts">
<div class="panel-heading">Services</div>
<div class="panel-body"><p>
{% for contract in inv_contracts %}
{{ contract.description }}<br />
{{ contract.units }}<br />
{% if not contract.discount == 0 %}
Discount %{{ contract.discount }}<br />
Credit: {{ contract.credit }}<br /><br />
{% endif %}
{% endfor %}
<p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default" id="contracts">
<div class="panel-heading">Domains</div>
<div class="panel-body"><p>
{% for domain in inv_domains %}
{{ fqdn }}<br />
{% endfor %}
<p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="panel-group">
<div class="panel panel-default" id="deploys">
<div class="panel-heading">Deployments</div>
<div class="panel-body"><p>
{% for deploy in inv_deployments %}
<ul class="nav nav-pills">
<li class="active"><a data-toggle="pill" href="#home{{ deploy.machine_id }}">{{ deploy.machine_alias }}</a></li>
<li><a data-toggle="pill" href="#control{{ deploy.machine_id }}">Control</a></li>
<li><a data-toggle="pill" href="#stats{{ deploy.machine_id }}">Stats</a></li>
<li><a data-toggle="pill" href="#keys{{ deploy.machine_id }}">Keys</a></li>
</ul>
<div class="tab-content">
<div id="home{{ deploy.machine_id}}" class="tab-pane fade in active">
<br />
</div>
<div id="control{{ deploy.machine_id}}" class="tab-pane fade">
<br />
{% if status[deploy.machine_id] == 'running' %}
<button class="btn btn-default btn-info" onclick="window.open('/vmvnc/{{ deploy.machine_id }}', '_blank');"><span class="glyphicon glyphicon-console" aria-hidden="true"></span> Console</button>
<button class="command command-vmshutdown btn btn-default btn-warning" value="vmshutdown" vmid="{{ deploy.machine_id }}"><span class="glyphicon glyphicon-off" aria-hidden="true"></span> Shutdown</button>
<button class="command command-vmstop btn btn-default btn-danger" value="vmstop" vmid="{{ deploy.machine_id }}"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span> Force Stop</button>
{% else %}
<button class="command command-vmstart btn btn-default btn-success" value="vmstart" vmid="{{ deploy.machine_id }}"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Start</button>
{% endif %}
<br /><br />
Grid ID# {{ deploy.machine_id }}<br >
CPU: {{ deploy.machine_cpu }} cores</br >
Memory: {{ deploy.machine_mem }} MB</br >
Disk Space: {{ deploy.machine_hdd }} GB</br >
<br />
Expiry date: {{ deploy.date_expire }}</br >
Credit: {{ deploy.credit }}
</div>
<div id="stats{{ deploy.machine_id}}" class="tab-pane fade">
<table class="tg">
<tr>
<th class="tg-yw4l">Network Bandwidth</th>
</tr>
<tr>
<td class="tg-yw4l"><img src="data:image/png;base64,{{ rrd[deploy.machine_id]['net'] }}" class="img-responsive"><br></td>
</tr>
</table>
<table class="tg">
<tr>
<th class="tg-yw4l">CPU Load</th>
</tr>
<tr>
<td class="tg-yw4l"><img src="data:image/png;base64,{{ rrd[deploy.machine_id]['cpu'] }}" class="img-responsive"><br></td>
</tr>
</table>
<table class="tg">
<tr>
<th class="tg-yw4l">Memory Usage</th>
</tr>
<tr>
<td class="tg-yw4l"><img src="data:image/png;base64,{{ rrd[deploy.machine_id]['mem'] }}" class="img-responsive"><br></td>
</tr>
</table>
<table class="tg">
<tr>
<th class="tg-yw4l">Disk Input/Output</th>
</tr>
<tr>
<td class="tg-yw4l"><img src="data:image/png;base64,{{ rrd[deploy.machine_id]['hdd'] }}" class="img-responsive"><br></td>
</tr>
</table>
</div>
<div id="keys{{ deploy.machine_id}}" class="tab-pane fade">
Not yet implemented.
</div>
</div>
<br />
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block title %}Deploy{% endblock %}
{% block page_content %}
<script>
window.onload =$(function(){
$("#noUiSlider").empty().noUiSlider( 'init', {
start: 0,
step: 1,
format: wNumb({
decimals: 0
}),
range: {
min: 1,
max: 16
}
});
});
</script>
<div class="page-header">
<h1>Deploy</h1>
</div>
<div class="container-fluid">
<br />
<div class="row">
<div class="col-md-4">
<div class="panel panel-info">
<div class="panel-heading">{{ product_name }}</div>
<div class="panel-body">
<img src="{{ product_pic }}"></img><br />
{{ product_description }}
</div>
</div>
</div>
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">Настройки</div>
<div class="panel-body">
<form method="POST" action="{{ url_for('proxadmin.deploy', product_id=product_id) }}">
<p>
{{ form.servername.label }}<br />{{ form.servername(size=42) }}<br />
{% for error in form.servername.errors %}
{{ error }}<br />
{% endfor %}
</p>
<br />
<p>
{{ form.region.label }}<br /> {{ form.region }}<br />
{% for error in form.region.errors %}
{{ error }}<br />
{% endfor %}
</p>
<br />
<p>
{{ form.vmpassword.label }}<br /> {{ form.vmpassword }}<br />
{% for error in form.vmpassword.errors %}
{{ error }}<br />
{% endfor %}
</p>
<br />
<p>
<div id="noUiSlider" class="noUiSlider"></div>
<div id="valueInput">
{{ form.cpu.label }} {{ form.cpu(class_="noUiSluder") }}<br />
</div>
{{ form.mem.label }} {{ form.mem }}<br />
{{ form.hdd.label }} {{ form.hdd }}<br />
{{ form.recipe.label }} {{ form.recipe }}<br />
<p>
<br />
{{ form.invite_key.label }} {{ form.invite_key }}<br />
{% for error in form.invite_key.errors %}
{{ error }}<br />
{% endfor %}
</div>
</div>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,5 @@
<p>{{ user.email }} encountered an error working with cube id: {{ cubeid }}<br />
<br />
</p>
<p>Regards,
Proxadmin</p>

View file

@ -0,0 +1,4 @@
User {{ user.email }} encountered an error working with cube id: {{ cubeid }}
Regards,
Proxadmin

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block page_content %}
<h1>Dashboard</h1>
<center>
<div class="row">
Празно е :( Можете да посетите <a href='/market/0'>магазинчето</a>
</div>
{% endblock %}

View file

@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block page_content %}
<div id="container">
<div class="container">
<div class="row">
<img src="../../static/images/robot.png" class="img-responsive"></img>
</div>
<div class="container-fluid-index">
<div class="row">
<div class="col-md-6">
<div class="panel-body">
<img src="../../static/images/VPS-equipment.png" width="150" height="150" />
<h2 class="media-heading">Оборудване</h2>
<p>Благодарение на внедрените нови технологии ние предлагаме изчислителна мощ, надеждно съхранение на данни и гъвкаво разпределение на ресурсите.</p>
</div>
<div class="panel-body">
<img src="../../static/images/VPS-Support.png" width="150" height="150" />
<h2 class="media-heading">Бърз приятелски support</h2>
<p>Ще Ви помогнем във всички неприятни ситуации, по всяко време. Независимо от нивото на умения Ви. Ние сме винаги насреща за въпроси.</p>
</div>
</div>
<div class="col-md-6">
<div class="panel-body">
<img src="../../static/images/VPS-Mission.png" width="150" height="150" />
<h2 class="media-heading">Мисия</h2>
<p>Основната ни цел е безкомпромисно качество на услугите ни. Предлагаме цялостна поддръжка на вашите машини, включително тези които използвате във Вашият офис.</p>
</div>
<div class="panel-body">
<img src="../../static/images/VPS-Security.png" width="150" height="150" />
<h2 class="media-heading">Сигурност и надеждност</h2>
<p>Разчитаме на собствено сървърно помещение, обезпечено от най- големите български доставчици на интернет свързаност. Предлагаме пълен достъп и наблюдение на процесите в реално време.</p>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block page_content %}
<iframe src="https://kiwiirc.com/client/irc.datapoint.bg:+6697/?nick=client|?&theme=relaxed#support" style="border:0; width:100%; height:640px;"></iframe>
{% endblock %}
{% block footer %}
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block page_content %}
<h1>App Store</h1>
<center>
<div class="row">
{% for key, value in products.items() %}
<div class="col-md-4">
<p><a href="/deploy/{{ key }}"><img src="{{ value['img'] }}"></img><br/>{{ value['name'] }}</a><br/>{{ value['description'] }}</p>
</div>
{% if key|int % 3 == 0 %}
</div>
<div class="row">
{% endif %}
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block page_content %}
<h1>{{ groupname }}</h1>
<center>
<div class="row">
{% for key, value in products.items() %}
<div class="col-md-4">
<p><a href="/deploy/{{ key }}"><img src="{{ value['img'] }}"></img><br/>{{ value['name'] }}</a><br/>{{ value['description'] }}</p>
</div>
{% if key|int % 3 == 0 %}
</div>
<div class="row">
{% endif %}
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block page_content %}
<h1>Правила и Условия</h1>
<center>
<div class="row">
<img src="../../static/images/datapoint.png" class="img-responsive"></img>
</div>
</center>
<h3 class="h3_paa" style="text-align: left;"> I. ОСНОВНИ ДЕФИНИЦИИ:</h3>
<p>Чл.1 (1) Информация съгласно Закона за електронната търговия и Закона за защита на потребителите:</p>
<p>Новахостинг ЕООД, ЕИК 202555287, адрес на управление: гр. Пловдив, ул. Волга №52., наричана по- долу ДОСТАВЧИК.</p>
<p>(2) КЛИЕНТ- физическо или юридическо лице, което използва услугите на ДОСТАВЧИКА.</p>
<p>(3) СЪРВЪР &#8211; Компютърна конфигурация или изчислителна система за съхранение и изпълнение на потребителски приложения и потребителска информация.</p>
<p>(8)Политика за използване на бисквитки (cookies)</p>
<p>Cookies (бисквитки) &#8211; малки текстови файлове, чрез инсталирането на които, НОВАХОСТИНГ ЕООД осигурява най-добра възможност за ползване на уебсайта си, чрез съхраняване на информация за потребителите, възстановяване на информацията за потребителя, проследяване на действия, идентификация и други сходни дейности, например съхранение на информация за услугите, от които се интересува потребителя, с цел предоставяне на подходяща информация при посещението му на Уебсайта.</p>
<p>(9) ВИРТУАЛЕН СЪРВЪР &#8211; Свободно пространство на сървър на ДОСТАВЧИКА, на което Клиента има право да инсталира приложения и да помества информация, която да споделя с трети лица в Интернет.</p>
<p>&nbsp;</p>
<h3></h3>
<h3 class="h3_paa"> II.ОБЩИ УСЛОВИЯ:</h3>
<p>&nbsp;</p>
<p>Чл.2 (1) Клиентът се съгласява с настоящите общи условия и се съгласява да ги спазва чрез едно от следните действия:</p>
<p>1. Избиране на отметка &#8220; Съгласен съм с общите условия&#8220;</p>
<p>2. При закупуване на услуга от ДОСТАВЧИКА.</p>
<p>(3) Избирането на отметка &#8216;Съгласен съм&#8217;, от КЛИЕНТА се счита за електронно изявление по смисъла на Закона за електронния документ и електронния подпис (ЗЕДЕП), като със записването му на съответен носител в сървъра на ДОСТАВЧИКА, електронното изявление придобива качеството на електронен документ по смисъла на цитирания закон.</p>
<p>(4) ДОСТАВЧИКЪТ записва чрез общоприет стандарт за преобразуване, по технически начин, правещ възможно възпроизвеждането и съхраняването в специални файлове (лог-файлове, Log files) на своя сървър IP адреса на КЛИЕНТ, както и всяка друга информация, необходима за идентифициране на КЛИЕНТ и възпроизвеждане на електронното му изявление за приемане на Общите условия, в случай на възникване на правен спор.</p>
<p>(3) Договорът за предоставяне на хостинг услуги между ДОСТАВЧИКА и КЛИЕНТА се счита за сключен при едно от събитията упоменати в Чл.2,</p>
<p>(4) Доставчикът си запазва правото да допълва, променя и редактира настоящите ОБЩИ УСЛОВИЯ.</p>
<h3 class="h3_paa"> III. Основни положения</h3>
<p>Чл.3 (1) Въз основа на настоящите Общи условия, ДОСТАВЧИКЪТ предоставя на КЛИЕНТА хостинг услуга, наричана по-долу за краткост &#8216;УСЛУГАТА&#8217;, представляваща правото на ползване на ресурсите на определена компютърна конфигурация &#8211; сървър, предостъпване на част от неговото дисково пространство, изчислителни ресурси и софтуер за управление чрез които КЛИЕНТА предоставя желана от него информация да бъде достъпна от други компютри през Интернет.</p>
<h3 class="h3_paa"> IV. Права и задължения на страните</h3>
<h4> Задължения на Доставчика:</h4>
<p>Чл.4 (1) ДОСТАВЧИКЪТ се задължава да поддържа постоянно техническите параметри на услугите, закупени от КЛИЕНТА.</p>
<p>(2) ДОСТАВЧИКЪТ се задължава да предостави на КЛИЕНТА техническа възможност да променя паролите и кодовете си за достъп до системата, като се задължава да не ги предоставя на трети лица</p>
<p>(3) ДОСТАВЧИКЪТ има право да прави софтуерни и хардуерни промени по техническото оборудване, като предупреди КЛИЕНТА за евентуални технически проблеми и прекъсвания на услугата.</p>
<p>(4) ДОСТАВЧИКЪТ се задължава да пази данните и файловете на КЛИЕНТА и да не допуска да стават достояние на трети лица, дори и след прекратяване на взаимоотношенията между двете страни.</p>
<p>(5) При констатирани нарушения или неспазване на клаузите по настоящите ОБЩИ УСЛОВИЯ, ДОСТАВЧИКЪТ има право да спре услугите, закупени от КЛИЕНТА без предупреждение.</p>
<p>&nbsp;</p>
<h4> Права на ДОСТАВЧИКА:</h4>
<p>Чл.5 (1) ДОСТАВЧИКЪТ не носи отговорност спрямо КЛИЕНТА в случаите, когато:</p>
<p>(2) УСЛУГАТА, закупена от клиента е с нарушени показатели, заради регулярни или инцидентни дейности по оборудването на ДОСТАВЧИКА, с цел подобряването качесвото на УСЛУГАТА.</p>
<p>(3) ДОСТАВЧИКЪТ има право да спре достъпа до услугата, използвана от клиента поради неплащане на задължение в срок от страна на КЛИЕНТА.</p>
<p>(4) e налице проблем със свързаността на ДОСТАВЧИКА с Интернет, поради локални или глобални проблеми с ресурси, извън мрежата на ДОСТАВЧИКА или неработоспособност на Интернет мрежата, преносната среда или оборудване между ДОСТАВЧИКА и КЛИЕНТА.</p>
<p>(5) КЛИЕНТЪТ не спазва изискванията и указанията на ДОСТАВЧИКА за нормалното функциониране на УСЛУГАТА.</p>
<p>(6) КЛИЕНТЪТ използва непозволен софтуер, който вреди на оборудването на ДОСТАВЧИКА и петни името му.</p>
<h4><strong> ПРАВА И ЗАДЪЛЖЕНИЯ НА КЛИЕНТА</strong></h4>
<p>&nbsp;</p>
<p>Чл.5 (1) КЛИЕНТЪТ е длъжен да заплаща заявените за използване услуги в срок, определен от ДОСТАВЧИКА.</p>
<p>(2) КЛИЕНТЪТ се задължава да не нарушава законовите разпоредби на Република България при използването на УСЛУГАТА.</p>
<p>(3) Докато използва услугата, КИЛЕНТЪТ се задължава да не нарушава под никаква форма права и законни интереси на ДОСТАВЧИКА и/или трети лица.</p>
<p>(4) КЛИЕНТЪТ се задължава да не предприема каквито и да било действия чрез УСЛУГАТА, които са в нарушение на законите на Република България или която и да е друга страна по света,</p>
<p>(5) КЛИЕНТЪТ няма право да публикува текстове и съобщения, съдържащи заплаха за физическата цялост и телесния интегритет на индивида, накърняващи доброто име на другиго или призоваващи към насилствена промяна на конституционния ред, към извършване на престъпление, към насилие над личността или към разпалване на расова, национална, етническа или религиозна вражда.</p>
<p>(6) КЛИЕНТЪТ се задължава да не използва услугите, предоставяни от ДОСТАВЧИКА за доставяне на нежелани търговски съобщения &#8211; СПАМ. При констатиран такъв случай, ДОСТАВЧИКЪТ има право да спре предоставяната на КЛИЕНТА услуга без предупреждение.</p>
<p>(7) КЛИЕНТЪТ се задължава при използване на предоставяната от ДОСТАВЧИКА УСЛУГА да не зарежда, да не разполага на сървър на ДОСТАВЧИКА, да не изпраща или използва по какъвто и да било начин и да не прави достояние на трети лица информация, данни, текст, звук, файлове, софтуер, музика, фотографии, графики, видео или аудио материали, съобщения, както и всякакви други материали:</p>
<ul>
<li>противоречащи на българското законодателство, приложимите чужди закони, настоящите Общи условия, Интернет етиката или добрите нрави;</li>
<li>съдържащи заплаха за живота и телесната неприкосновеност на човека;</li>
<li>пропагандиращи дискриминация, основана на пол, раса, образователен ценз, възраст и религия; проповядващи фашистка, расистка или друга недемократична идеология;</li>
<li>с порнографско съдържание или каквото и да било друго съдържание, което застрашава нормалното психическо развитие на не навършилите пълнолетие лица или нарушава нормите на морала и добрите нрави;</li>
<li>съдържащи детска порнография, сексуално насилие, както хипервръзки към страници с подобно съдържание;</li>
<li>чието съдържание нарушава права или свободи на човека съгласно Конституцията и законите на Република България или международни актове, по които Република България е страна;</li>
<li>представляващи търговска, служебна или лична тайна или друга конфиденциална информация;</li>
<li>предмет на авторското право, освен в случаите на притежаване на това право или със съгласието на неговия носител;</li>
<li>нарушаващи каквито и да били имуществени или не имуществени права или законни интереси на трети лица, включително право на собственост, право на интелектуална собственост и други;</li>
<li>накърняващи доброто име на другиго и призоваващи към насилствена промяна на конституционно установения ред, към извършване на престъпление, към насилие над личността или към разпалване на расова, национална, eтническа или религиозна вражда;</li>
<li>съдържащи информация за чужди пароли или права за достъп без съгласието на техния титуляр, както и софтуер за достъп до такива пароли или права.</li>
</ul>
<p>&nbsp;</p>
<p style="text-align: left;">Забранено е разпространението, съхранението или излагането на данни, материали или информация, които нарушават законите на Република България и/или законите на други държави. Това включва, но не се изчерпва с: материали, защитени от законите за авторски права; материали, които са заплашителни или нецензурни; материали, които са предназначени за лица над 18 години (&#8222;само за възрастни&#8220;); материали, защитени като фирмена тайна или с друг статут ограничаващ тяхното публично разпространение.</p>
<p style="text-align: left;">Потребителя се задължава да плати обезщетение и поеме вината при използване на услугите.</p>
<p style="text-align: left;">Използването на услугите за нарушаване на защитени материали и търговски марки е забранено. Това включва, но не се изчерпва с неоторизирано копиране на музика, книги, снимки и всякакви други защитени продукти. Използването на акаунта за продаване на фалшификати на регистрирани търговски марки ще доведе до незабавното изтриване на потребителския акаунт. Ако се установи, че акаунта на потребителя нарушава други запазени права то достъпа до защитените материали ще бъде преустановен. Всеки потребителски акаунт хванат в повторно нарушение ще бъде спрян и/или изтрит от сървърите на доставчика. Ако смятате, че Вашите запазени права са нарушени може да пишете до доставчика на адрес с необходимата информация.</p>
</div>
{% endblock %}

View file

@ -0,0 +1,10 @@
<div class="col-md-4">
<div class="panel panel-info">
<div class="panel-heading">{{ current_user.name }}</div>
<div class="panel-body">
<a href="https://en.gravatar.com/site/signup/"><img class="roundavatar" src="{{ current_user.gravatar(128) }}"></img></a><br />
2-Factor: {{ current_user.twofactor }}<br />
</div>
</div>
</div>

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block title %}Зареждане на сметка{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Фактуриране</h1>
</div>
<div class="container-fluid">
<br />
<div class="row">
{% block sidebar %}
{% include "uinvoice/_sidebar.html" %}
{% endblock %}
<div class="col-md-8">
<div class="panel panel-info">
<div class="panel-heading">Зареждане на сметка</div>
<div class="panel-body">
<form method="POST" action="{{ url_for('uinvoice.charge') }}">
<p>
{{ form.invoice_amount.label }} {{ form.invoice_amount }}<br />
{% for error in form.invoice_amount.errors %}
{{ error }}<br />
{% endfor %}
</p>
</div>
</div>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Фактури{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Фактури</h1>
</div>
<div class="container-fluid">
<br />
<div class="row">
{% block sidebar %}
{% include "uinvoice/_sidebar.html" %}
{% endblock %}
<div class="col-md-8">
<div class="panel panel-info">
<div class="panel-heading">Издадени Фактури</div>
<div class="panel-body">
<p>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Document ID</th>
<th>Date</th>
<th>Amount</th>
<th> </th>
</tr>
</thead>
<tbody>
{% for invoice in documents %}
{% if invoice.paid %}
<tr class="success">
<td>{{ invoice.pid }}</td>
<td>{{ invoice.date_created.strftime('%d %b %Y - %H:%m') }}</td>
<td>{{ invoice.amount }}</td>
<td><a href='invoice/{{ invoice.pid }}'>Preview</a></td>
<td> </td>
{% else %}
<tr class="danger">
<td>{{ invoice.pid }}</td>
<td>{{ invoice.date_created.strftime('%d %b %Y - %H:%m') }}</td>
<td>{{ invoice.amount }}</td>
<td><a href='invoice/{{ invoice.pid }}'>Pay</a></td>
{% endif %}
</tr>
</tbody>
{% endfor %}
</table>
</div>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,4 @@
<p>{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }} <br />
</p>
<p>Regards,
Proxadmin</p>

View file

@ -0,0 +1,4 @@
{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }}
Regards,
Proxadmin

View file

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}Фактура No. #{{ document.pid }}{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Фактура No. #{{ document.pid }}</h1>
</div>
<div class="container-fluid">
<br />
<div class="row">
{% block sidebar %}
{% include "uinvoice/_sidebar.html" %}
{% endblock %}
<div class="col-md-8">
<div class="panel panel-info">
<div class="panel-heading"></div>
<div class="panel-body">
<form method="POST" action="{{ url_for('uinvoice.invoice', document_id=document.pid) }}">
<p>
{{ document.invoice_date }}<br />
{{ document.invoice_amount }}<br />
print to pdf.
</p>
{% if not document.paid %}
<p>
{{ form.processor.label }} {{ form.processor }} <br />
</p>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% block title %}Edit Profile{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Edit Your Profile</h1>
</div>
<div class="container-fluid">
<br />
<div class="row">
{% block sidebar %}
{% include "uinvoice/_sidebar.html" %}
{% endblock %}
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading">Данни на профила</div>
<div class="panel-body">
<form method="POST" action="{{ url_for('uinvoice.profile') }}">
<p>
{{ form.name.label }}<br />{{ form.name(size=42) }}<br />
{% for error in form.name.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.address.label }}<br /> {{ form.address(size=42) }}<br />
{% for error in form.address.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.city.label }}<br />{{ form.city(size=42) }}<br />
{% for error in form.city.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.postcode.label }}<br />{{ form.postcode(size=24) }}<br />
{% for error in form.postcode.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.country.label }}<br /> {{ form.country }}<br />
{% for error in form.country.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.phone.label }}<br /> {{ form.phone(size=42) }}<br />
{% for error in form.phone.errors %}
{{ error }}<br />
{% endfor %}
</p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Данни за юридическо лице</div>
<div class="panel-body">
<p>
{{ form.org_responsible.label }}<br />{{ form.org_responsible(size=42) }}<br />
{% for error in form.org_responsible.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.org_bulstat.label }}<br />{{ form.org_bulstat(size=42) }}<br />
{% for error in form.org_bulstat.errors %}
{{ error }}<br />
{% endfor %}
<p>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Допълнителна защита на акаунта (2-Factor Authentication)</div>
<div class="panel-body">
<p>
<p>За да използвате тази функция изпълнете следните стъпки:<br />
1. Моля инсталирайте <a href="https://fedorahosted.org/freeotp/">FreeOTP</a> на вашият смартфон. <a href="https://itunes.apple.com/us/app/freeotp/id872559395">iTunes</a> | <a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">Google Play</a><br />
2. Сканирайте с помоща на приложението вашия QR код. Той е равносилен на допълнителна парола и не трябва да бъде показван или изгубван<br />
<input type="button" value="Покажи QR кода" onclick="window.open('{{ url_for('auth.qrcode') }}','popUpWindow','height=500,width=400,left=100,top=100,resizable=yes,scrollbars=yes,toolbar=yes,menubar=no,location=no,directories=no, status=yes');"><br />
3. Маркирайте отметката и обновете профила. Не губете своя QR код. </p>
{{ form.twofactor }} {{ form.twofactor.label }}<br />
</p>
</div>
</div>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,4 @@
from flask import Blueprint
uinvoice = Blueprint('uinvoice', __name__)
from . import routes

View file

@ -0,0 +1,68 @@
from iso3166 import countries
import string
import random
from ..models import User, Role
from flask_wtf import FlaskForm, RecaptchaField
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
from wtforms import validators, ValidationError
from wtforms.fields.html5 import EmailField
class EditProfileForm(FlaskForm):
name = StringField('Лице за контакт:', [validators.DataRequired(), validators.Length(3, 60)])
address = StringField('Адрес:', [validators.DataRequired(), validators.Length(2, 50)])
city = StringField('Град:', [validators.DataRequired(), validators.Length(2,40)])
postcode = StringField('Пощенски Код:')
clist = []
for c in countries:
clist.append((c.alpha2, c.name))
country = SelectField('Държава:', choices=clist, default='BG')
phone = StringField('Телефон:')
org_responsible = StringField('Отговорно Лице:')
org_bulstat = StringField('БУЛСТАТ:')
twofactor = BooleanField('2-factor authentication')
submit = SubmitField('Обнови')
class EditProfileAdminForm(FlaskForm):
email = StringField('Електроннa поща (логин):', [validators.DataRequired(), validators.Length(1, 64), validators.Email()])
confirmed = BooleanField('Активиран')
role = SelectField('Роля', coerce=int)
name = StringField('Лице за контакт:', [validators.DataRequired(), validators.Length(3, 60)])
address = StringField('Адрес:', [validators.DataRequired(), validators.Length(2, 50)])
city = StringField('Град:', [validators.DataRequired(), validators.Length(2,40)])
postcode = DecimalField('Пощенски Код:')
clist = []
for c in countries:
clist.append((c.alpha2, c.name))
country = SelectField('Държава:', choices=clist)
phone = DecimalField('Телефон:', [validators.DataRequired()])
org_responsible = StringField('Отговорно Лице:')
org_bulstat = StringField('БУЛСТАТ:')
submit = SubmitField('Обнови')
def __init__(self, user, *args, **kwargs):
super(EditProfileAdminForm, self).__init__(*args, **kwargs)
self.role.choices = [(role.pid, role.name)
for role in Role.query.order_by(Role.name).all()]
self.user = user
def validate_email(self, field):
if field.data != self.user.email and User.query.filter_by(email=field.data).first():
raise ValidationError('Email-а е вече регистриран.')
class ChargeForm(FlaskForm):
invoice_amount = DecimalField('Стойност:', [validators.DataRequired(), validators.NumberRange(min=0, max=6)])
submit = SubmitField('Зареди')
class PaymentForm(FlaskForm):
plist = [('paypal', 'PayPal'), ('epay', 'ePay.bg'), ('bank', 'Bank Transfer')]
processor = SelectField('Финансов инструмент:', choices=plist)
submit = SubmitField('Плати')

View file

@ -0,0 +1,102 @@
from flask import render_template, redirect, request, url_for, flash, session, abort, current_app
from flask_login import login_required, login_user, logout_user, current_user
from sqlalchemy import desc
from . import uinvoice
from .forms import EditProfileForm, EditProfileAdminForm, ChargeForm, PaymentForm
from ..email import send_email
from .. import db
from ..models import User, Order
#PROFILE
@uinvoice.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
page = { 'title': 'Edit Profile' }
currentmail = current_user.email
ouruser = User.query.filter_by(email=currentmail).first()
db.session.commit()
#wallet = "%.2f" % round(ouruser.wallet, 3)
#print(wallet)
form = EditProfileForm()
if form.validate_on_submit():
current_user.name = form.name.data
current_user.address = form.address.data
current_user.city = form.city.data
current_user.postcode = form.postcode.data
current_user.country = form.country.data
current_user.phone = form.phone.data
current_user.org_responsible = form.org_responsible.data
current_user.org_bulstat = form.org_bulstat.data
current_user.twofactor = form.twofactor.data
db.session.add(current_user)
db.session.commit()
flash('Info Updated!')
form.twofactor.data = current_user.twofactor
form.name.data = current_user.name
form.address.data = current_user.address
form.city.data = current_user.city
form.postcode.data = current_user.postcode
form.country.data = current_user.country
form.phone.data = current_user.phone
form.org_responsible.data = current_user.org_responsible
form.org_bulstat.data = current_user.org_bulstat
return render_template('uinvoice/profile.html', page=page, form=form)
#INVOICES
#@uinvoice.route('/charge', methods=['GET', 'POST'])
#@login_required
#def charge():
# """ generate new invoice based on user request """
# unpaid_invoices = Order.query.filter_by(user_id=current_user.pid).filter_by(paid=False).all()
# if unpaid_invoices != []:
# flash('You have unpaid invoices')
# return redirect(url_for('uinvoice.documents'))
# page = { 'title': 'Charge Funds' }
# form = ChargeForm()
# if form.validate_on_submit():
# newinvoice = Order(amount=form.invoice_amount.data, user_id=current_user.pid)
# db.session.add(newinvoice)
# db.session.commit()
# return redirect(url_for('uinvoice.documents'))
# return render_template('uinvoice/charge.html', page=page, form=form)
@uinvoice.route('/documents', methods=['GET'])
@login_required
def documents():
page = { 'title': 'Order documents' }
invoices = Order.query.filter_by(user_id=current_user.pid).order_by(desc(Order.date_created)).all()
db.session.commit()
return render_template('uinvoice/documents.html', page=page, documents=invoices)
@uinvoice.route('/order/<int:document_id>', methods=['GET', 'POST'])
@login_required
def order(document_id):
page = { 'title': 'Preview ' + str(document_id) }
order = Order.query.filter_by(pid=document_id).first()
db.session.commit()
#check if document_id is owned by you.
try:
if order.user_id != current_user.pid:
print('WARNING: user {} violates order {}'.format(current_user.pid, order.pid))
abort(404)
except:
abort(404)
form = PaymentForm()
if form.validate_on_submit():
#TODO: contact payment processor
send_email(current_app.config['MAIL_USERNAME'], current_user.email + ' plati ' + str(order.units * order.unitvalue) + ' v koshnicata.', 'uinvoice/email/adm_payment', user=current_user, order=order )
order.paid = True
return redirect(url_for('uinvoice.documents'))
#except:
# abort(404)
return render_template('uinvoice/invoice.html', page=page, form=form, document=invoice, document_id=document_id)

17
requirements.txt Normal file
View file

@ -0,0 +1,17 @@
Flask
Flask-Login
Flask-Bootstrap
Flask-WTF
Flask-Script
Flask-Migrate
Flask-Mail
Flask-SQLAlchemy
Flask-Babel
psycopg2
itsdangerous
requests
sortedcontainers
iso3166
pyqrcode
onetimepass
schedule

Some files were not shown because too many files have changed in this diff Show more