initial commit
18
LICENSE
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
||||
|
4
proxadmin/auth/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from flask import Blueprint
|
||||
auth = Blueprint('auth', __name__)
|
||||
from . import routes
|
||||
|
51
proxadmin/auth/forms.py
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
class ValidationError(ValueError):
|
||||
pass
|
415
proxadmin/models.py
Normal 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
|
||||
|
3
proxadmin/proxadmin/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
proxadmin = Blueprint('proxadmin', __name__)
|
||||
from . import routes
|
33
proxadmin/proxadmin/errors.py
Normal 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
|
||||
|
24
proxadmin/proxadmin/forms.py
Normal 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')
|
||||
|
||||
|
245
proxadmin/proxadmin/routes.py
Normal 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)
|
||||
|
||||
|
277
proxadmin/static/css/bootstrap-slider.css
vendored
Normal 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;
|
||||
}
|
94
proxadmin/static/css/navbar.css
Normal 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;
|
||||
}
|
||||
}
|
278
proxadmin/static/css/nouislider.css
Normal 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%;
|
||||
}
|
100
proxadmin/static/css/rangeslider.scss
Normal 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));
|
||||
}
|
152
proxadmin/static/css/style.css
Normal 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;
|
||||
}
|
||||
|
121
proxadmin/static/css/style.css.old
Normal 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;
|
||||
}
|
||||
|
BIN
proxadmin/static/images/1.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
proxadmin/static/images/2.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
proxadmin/static/images/220x180.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
proxadmin/static/images/3.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
proxadmin/static/images/4.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
proxadmin/static/images/5.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
proxadmin/static/images/6.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
proxadmin/static/images/Hosting2.png
Normal file
After Width: | Height: | Size: 424 KiB |
BIN
proxadmin/static/images/VPS-Mission.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
proxadmin/static/images/VPS-Security.png
Normal file
After Width: | Height: | Size: 7 KiB |
BIN
proxadmin/static/images/VPS-Support.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
proxadmin/static/images/VPS-equipment.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
proxadmin/static/images/_bg.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
proxadmin/static/images/bg-linear.jpg
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
proxadmin/static/images/bg.jpg
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
proxadmin/static/images/datapoint.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
proxadmin/static/images/header-layer.jpg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
proxadmin/static/images/hex24.png
Normal file
After Width: | Height: | Size: 999 B |
BIN
proxadmin/static/images/hex32.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
proxadmin/static/images/hex512.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
proxadmin/static/images/purplebg.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
proxadmin/static/images/purplebg1.jpg
Normal file
After Width: | Height: | Size: 179 KiB |
BIN
proxadmin/static/images/purplebg2.jpg
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
proxadmin/static/images/robot.png
Normal file
After Width: | Height: | Size: 402 KiB |
BIN
proxadmin/static/images/texture-diagonal.png
Normal file
After Width: | Height: | Size: 85 B |
1807
proxadmin/static/js/bootstrap-slider.js
vendored
Normal file
4
proxadmin/static/js/jquery.js
vendored
Normal file
1850
proxadmin/static/js/mgui.js
Normal file
3
proxadmin/static/js/nouislider.min.js
vendored
Normal file
493
proxadmin/static/js/rangeslider.js
Normal 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);';
|
||||
|
||||
}));
|
12
proxadmin/templates/_formhelpers.html
Normal 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 %}
|
57
proxadmin/templates/_slide.html
Normal 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 %}
|
15
proxadmin/templates/auth/2fa.html
Normal 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 %}
|
20
proxadmin/templates/auth/already_confirmed.html
Normal 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 %}
|
15
proxadmin/templates/auth/change_password.html
Normal 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 %}
|
6
proxadmin/templates/auth/email/adm_loginnotify.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<p>{{ user.email }} logged in.<br />
|
||||
<br />
|
||||
IP Address: {{ ipaddr }}<br />
|
||||
</p>
|
||||
<p>Regards,
|
||||
Proxadmin</p>
|
6
proxadmin/templates/auth/email/adm_loginnotify.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
User {{ user.email }} logged in.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
6
proxadmin/templates/auth/email/adm_regnotify.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
New user {{ user.email }} has been registered.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
6
proxadmin/templates/auth/email/adm_regnotify.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
New user {{ user.email }} has been registered.
|
||||
|
||||
IP Address: {{ ipaddr }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
8
proxadmin/templates/auth/email/confirm.html
Normal 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>
|
13
proxadmin/templates/auth/email/confirm.txt
Normal 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.
|
5
proxadmin/templates/auth/email/reset_password.html
Normal 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>
|
9
proxadmin/templates/auth/email/reset_password.txt
Normal 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.
|
18
proxadmin/templates/auth/login.html
Normal 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 %}
|
17
proxadmin/templates/auth/register.html
Normal 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 %}
|
16
proxadmin/templates/auth/reset_password.html
Normal 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 %}
|
||||
|
22
proxadmin/templates/auth/unconfirmed.html
Normal 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 %}
|
44
proxadmin/templates/base.html
Normal 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 %}
|
||||
|
||||
|
16
proxadmin/templates/errors/403.html
Normal 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 %}
|
34
proxadmin/templates/errors/404.html
Normal 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 %}
|
26
proxadmin/templates/errors/500.html
Normal 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 %}
|
16
proxadmin/templates/errors/csrf_error.html
Normal 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 %}
|
9
proxadmin/templates/footer.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% block page_footer %}
|
||||
<br />
|
||||
<hr>
|
||||
<div id="footer">
|
||||
<p>
|
||||
© 2017 <a href="https://panel.datapoint.bg/aboutus">datapoint.bg</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
70
proxadmin/templates/nav-full.html
Normal 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 %}
|
||||
|
||||
|
60
proxadmin/templates/nav.html
Normal 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 %}
|
||||
|
||||
|
161
proxadmin/templates/proxadmin/_misc.html
Normal 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 %}
|
19
proxadmin/templates/proxadmin/aboutus.html
Normal 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;"> За да сме мотивирани, не просто работим. Вършим задачите си с удоволствие в най- приятния офис – малкият двор на “избата“. Това място за нас е свещенно – то ни зарежда и мотивира, там си почиваме, провеждаме работните си срещи и обсъждаме работата си.</p>
|
||||
<h3 style="text-align: left;">Екипът</h3>
|
||||
<p>Като модерна и устремена към успех компания, в нашата структура няма строго определено йерархично ниво. Вскички сме ХОРА, равни пред идеите си.</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
188
proxadmin/templates/proxadmin/dashboard.html
Normal 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 %}
|
||||
|
||||
|
113
proxadmin/templates/proxadmin/deploy.html
Normal 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 %}
|
||||
|
5
proxadmin/templates/proxadmin/email/adm_unreachable.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<p>{{ user.email }} encountered an error working with cube id: {{ cubeid }}<br />
|
||||
<br />
|
||||
</p>
|
||||
<p>Regards,
|
||||
Proxadmin</p>
|
4
proxadmin/templates/proxadmin/email/adm_unreachable.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
User {{ user.email }} encountered an error working with cube id: {{ cubeid }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
10
proxadmin/templates/proxadmin/empty_dashboard.html
Normal 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 %}
|
44
proxadmin/templates/proxadmin/index.html
Normal 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 %}
|
||||
|
9
proxadmin/templates/proxadmin/livechat.html
Normal 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 %}
|
18
proxadmin/templates/proxadmin/market.html
Normal 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 %}
|
18
proxadmin/templates/proxadmin/marketgroup.html
Normal 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 %}
|
75
proxadmin/templates/proxadmin/terms.html
Normal 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) СЪРВЪР – Компютърна конфигурация или изчислителна система за съхранение и изпълнение на потребителски приложения и потребителска информация.</p>
|
||||
<p>(8)Политика за използване на бисквитки (cookies)</p>
|
||||
<p>Cookies (бисквитки) – малки текстови файлове, чрез инсталирането на които, НОВАХОСТИНГ ЕООД осигурява най-добра възможност за ползване на уебсайта си, чрез съхраняване на информация за потребителите, възстановяване на информацията за потребителя, проследяване на действия, идентификация и други сходни дейности, например съхранение на информация за услугите, от които се интересува потребителя, с цел предоставяне на подходяща информация при посещението му на Уебсайта.</p>
|
||||
<p>(9) ВИРТУАЛЕН СЪРВЪР – Свободно пространство на сървър на ДОСТАВЧИКА, на което Клиента има право да инсталира приложения и да помества информация, която да споделя с трети лица в Интернет.</p>
|
||||
<p> </p>
|
||||
<h3></h3>
|
||||
<h3 class="h3_paa"> II.ОБЩИ УСЛОВИЯ:</h3>
|
||||
<p> </p>
|
||||
<p>Чл.2 (1) Клиентът се съгласява с настоящите общи условия и се съгласява да ги спазва чрез едно от следните действия:</p>
|
||||
<p>1. Избиране на отметка “ Съгласен съм с общите условия“</p>
|
||||
<p>2. При закупуване на услуга от ДОСТАВЧИКА.</p>
|
||||
<p>(3) Избирането на отметка ‘Съгласен съм’, от КЛИЕНТА се счита за електронно изявление по смисъла на Закона за електронния документ и електронния подпис (ЗЕДЕП), като със записването му на съответен носител в сървъра на ДОСТАВЧИКА, електронното изявление придобива качеството на електронен документ по смисъла на цитирания закон.</p>
|
||||
<p>(4) ДОСТАВЧИКЪТ записва чрез общоприет стандарт за преобразуване, по технически начин, правещ възможно възпроизвеждането и съхраняването в специални файлове (лог-файлове, Log files) на своя сървър IP адреса на КЛИЕНТ, както и всяка друга информация, необходима за идентифициране на КЛИЕНТ и възпроизвеждане на електронното му изявление за приемане на Общите условия, в случай на възникване на правен спор.</p>
|
||||
<p>(3) Договорът за предоставяне на хостинг услуги между ДОСТАВЧИКА и КЛИЕНТА се счита за сключен при едно от събитията упоменати в Чл.2,</p>
|
||||
<p>(4) Доставчикът си запазва правото да допълва, променя и редактира настоящите ОБЩИ УСЛОВИЯ.</p>
|
||||
<h3 class="h3_paa"> III. Основни положения</h3>
|
||||
<p>Чл.3 (1) Въз основа на настоящите Общи условия, ДОСТАВЧИКЪТ предоставя на КЛИЕНТА хостинг услуга, наричана по-долу за краткост ‘УСЛУГАТА’, представляваща правото на ползване на ресурсите на определена компютърна конфигурация – сървър, предостъпване на част от неговото дисково пространство, изчислителни ресурси и софтуер за управление чрез които КЛИЕНТА предоставя желана от него информация да бъде достъпна от други компютри през Интернет.</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> </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> </p>
|
||||
<p>Чл.5 (1) КЛИЕНТЪТ е длъжен да заплаща заявените за използване услуги в срок, определен от ДОСТАВЧИКА.</p>
|
||||
<p>(2) КЛИЕНТЪТ се задължава да не нарушава законовите разпоредби на Република България при използването на УСЛУГАТА.</p>
|
||||
<p>(3) Докато използва услугата, КИЛЕНТЪТ се задължава да не нарушава под никаква форма права и законни интереси на ДОСТАВЧИКА и/или трети лица.</p>
|
||||
<p>(4) КЛИЕНТЪТ се задължава да не предприема каквито и да било действия чрез УСЛУГАТА, които са в нарушение на законите на Република България или която и да е друга страна по света,</p>
|
||||
<p>(5) КЛИЕНТЪТ няма право да публикува текстове и съобщения, съдържащи заплаха за физическата цялост и телесния интегритет на индивида, накърняващи доброто име на другиго или призоваващи към насилствена промяна на конституционния ред, към извършване на престъпление, към насилие над личността или към разпалване на расова, национална, етническа или религиозна вражда.</p>
|
||||
<p>(6) КЛИЕНТЪТ се задължава да не използва услугите, предоставяни от ДОСТАВЧИКА за доставяне на нежелани търговски съобщения – СПАМ. При констатиран такъв случай, ДОСТАВЧИКЪТ има право да спре предоставяната на КЛИЕНТА услуга без предупреждение.</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> </p>
|
||||
<p style="text-align: left;">Забранено е разпространението, съхранението или излагането на данни, материали или информация, които нарушават законите на Република България и/или законите на други държави. Това включва, но не се изчерпва с: материали, защитени от законите за авторски права; материали, които са заплашителни или нецензурни; материали, които са предназначени за лица над 18 години („само за възрастни“); материали, защитени като фирмена тайна или с друг статут ограничаващ тяхното публично разпространение.</p>
|
||||
<p style="text-align: left;">Потребителя се задължава да плати обезщетение и поеме вината при използване на услугите.</p>
|
||||
<p style="text-align: left;">Използването на услугите за нарушаване на защитени материали и търговски марки е забранено. Това включва, но не се изчерпва с неоторизирано копиране на музика, книги, снимки и всякакви други защитени продукти. Използването на акаунта за продаване на фалшификати на регистрирани търговски марки ще доведе до незабавното изтриване на потребителския акаунт. Ако се установи, че акаунта на потребителя нарушава други запазени права то достъпа до защитените материали ще бъде преустановен. Всеки потребителски акаунт хванат в повторно нарушение ще бъде спрян и/или изтрит от сървърите на доставчика. Ако смятате, че Вашите запазени права са нарушени може да пишете до доставчика на адрес с необходимата информация.</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
10
proxadmin/templates/uinvoice/_sidebar.html
Normal 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>
|
||||
|
47
proxadmin/templates/uinvoice/charge.html
Normal 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 %}
|
||||
|
70
proxadmin/templates/uinvoice/documents.html
Normal 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 %}
|
||||
|
4
proxadmin/templates/uinvoice/email/adm_payment.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<p>{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }} <br />
|
||||
</p>
|
||||
<p>Regards,
|
||||
Proxadmin</p>
|
4
proxadmin/templates/uinvoice/email/adm_payment.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ user.email }} paid {{ order.units }} x {{ order.unitvalue }} = {{ order.units * order.unitvalue }}
|
||||
|
||||
Regards,
|
||||
Proxadmin
|
54
proxadmin/templates/uinvoice/invoice.html
Normal 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 %}
|
||||
|
123
proxadmin/templates/uinvoice/profile.html
Normal 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 %}
|
||||
|
4
proxadmin/uinvoice/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from flask import Blueprint
|
||||
uinvoice = Blueprint('uinvoice', __name__)
|
||||
from . import routes
|
||||
|
68
proxadmin/uinvoice/forms.py
Normal 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('Плати')
|
||||
|
102
proxadmin/uinvoice/routes.py
Normal 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
|
@ -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
|