import proxadmin as example app

This commit is contained in:
Daniel afx 2022-02-04 20:22:37 +02:00
parent df45adcf4c
commit fd02f807a9
139 changed files with 9409 additions and 41 deletions

View file

@ -1,57 +1,164 @@
import os
import sys
from werkzeug.utils import secure_filename
from flask import (
Flask,
g,
render_template,
jsonify,
send_from_directory,
request,
redirect,
url_for
)
from flask_sqlalchemy import SQLAlchemy
from flask.json import JSONEncoder
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_login import LoginManager
from flask_pagedown import PageDown
from flask_wtf.csrf import CSRFProtect, CSRFError
from flask_babel import Babel, lazy_gettext
from flask_moment import Moment
#from flask_httpauth import import import HTTPBasicAuth
from werkzeug.contrib.fixers import ProxyFix
from config import config
sys.stderr.write("worker uid={} gid={}".format(os.getuid(), os.getgid()))
sys.stderr.flush()
app = Flask(__name__)
app.config.from_object("project.config.Config")
app.config.from_object("forest.config.Config")
db = SQLAlchemy(app)
#db = SQLAlchemy(session_options = { "autoflush": False })
db.init_app(app)
#apiauth = HTTPBasicAuth()
lm = LoginManager()
lm.init_app(app)
lm.login_view = 'auth.login'
lm.login_message = 'Login Required.'
lm.session_protection = 'strong'
#lm.session_protection = 'basic'
mail = Mail()
mail.init_app(app)
bootstrap = Bootstrap()
bootstrap.init_app(app)
pagedown = PageDown(app)
csrf = CSRFProtect(app)
#csrf.init_app(app)
babel = Babel()
babel.init_app(app)
moment = Moment(app)
moment.init_app(app)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
from .panel import panel as panel_blueprint
app.register_blueprint(panel_blueprint, url_prefix='/panel')
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
from .admin import admin as admin_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/' + app.config['ADMIN_PREFIX'])
from .settings import settings as settings_blueprint
app.register_blueprint(settings_blueprint, url_prefix='/settings')
class CustomJSONEncoder(JSONEncoder):
"""This class adds support for lazy translation texts to Flask's
JSON encoder. This is necessary when flashing translated texts."""
def default(self, obj):
from speaklater import is_lazy_string
if is_lazy_string(obj):
try:
return unicode(obj) # python 2
except NameError:
return str(obj) # python 3
return super(CustomJSONEncoder, self).default(obj)
#app.json_encoder = CustomJSONEncoder
@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(503)
def service_unavailable(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'service unavailable'})
response.status_code = 503
return response
return render_template('errors/503.html'), 503
@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 not app.config['DEBUG'] == 1 and app.config['MAIL_SERVER'] != '':
import logging
from logging.handlers import SMTPHandler
credentials = None
secure = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
credentials = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
if app.config['MAIL_USE_TLS'] is None:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr=app.config['MAIL_SENDER'],
toaddrs=[app.config['MAIL_ADMIN']],
subject=app.config['MAIL_SUBJECT_PREFIX'] + ' Application Error',
credentials=credentials,
secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(128), unique=True, nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
def __init__(self, email):
self.email = email
@app.route("/")
def hello_world():
return jsonify(hello="world")
@app.route("/static/<path:filename>")
def staticfiles(filename):
return send_from_directory(app.config["STATIC_FOLDER"], filename)
@app.route("/media/<path:filename>")
def mediafiles(filename):
return send_from_directory(app.config["MEDIA_FOLDER"], filename)
@app.route("/upload", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
file = request.files["file"]
filename = secure_filename(file.filename)
file.save(os.path.join(app.config["MEDIA_FOLDER"], filename))
return """
<!doctype html>
<title>upload new File</title>
<form action="" method=post enctype=multipart/form-data>
<p><input type=file name=file><input type=submit value=Upload>
</form>
"""
if __name__ == '__main__':
app.run()

View file

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

View file

@ -0,0 +1,34 @@
import string
import random
from .. import db
from ..models import User, Role, Region
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, DecimalRangeField
class OrderForm(FlaskForm):
cpu = DecimalRangeField('Processor Cores', default=2)
memory = DecimalRangeField('Memory', default=512)
storage = DecimalRangeField('Storage', default=20)
alias = StringField('Machine Alias:', [validators.Regexp(message='ex.: 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})$'), validators.Length(6,64)])
submit = SubmitField('Create')
class ChargeForm(FlaskForm):
amount = DecimalField('Стойност:', [validators.DataRequired(), validators.NumberRange(min=1, max=500)])
submit = SubmitField('Зареди')
class Addr2PoolForm(FlaskForm):
#regions = Region.query.all()
#region_choices = []
#for region in regions:
# region_choices.expand((region.pid, str(region.description)))
region_choices = [(1, 'Plovdiv, Bulgaria')]
region = SelectField('Region', choices=region_choices, coerce=int)
ip = StringField('IP Address:', [validators.DataRequired(), validators.Regexp(message='172.16.0.1', regex='^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')])
rdns = StringField('Reverse DNS:', [validators.Optional(), validators.Regexp(message='must be fqdn', 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})$')])
reserved = BooleanField('Reserved:')
submit = SubmitField('Add IP')

View file

@ -0,0 +1,182 @@
from flask import render_template, abort, redirect, url_for, abort, flash, request, current_app, make_response, g
from flask_login import fresh_login_required, login_user, logout_user
from flask_sqlalchemy import get_debug_queries
from . import admin
from .forms import ChargeForm, Addr2PoolForm, OrderForm
from .. import db
from ..email import send_email
from ..models import User, Transaction, Order, Server, Deployment, Service, Region, Address, Domain, contact_proxmaster
from ..decorators import admin_required, permission_required
import base64
import string
import random
from datetime import datetime, timedelta, date, time
import ipaddress
#@admin.before_app_request
#def before_request():
# g.user = current_user
# print('current_user: %s, g.user: %s, leaving bef_req' % (current_user, g.user))
@admin.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
@admin.route("/", methods=['GET'])
@fresh_login_required
@admin_required
def index():
return redirect(url_for('admin.list_users'))
@admin.route("/listorders", methods=['GET', 'POST'])
@fresh_login_required
@admin_required
def list_orders():
NewOrders = Order.query.filter_by(status='new').order_by(Order.date_created.asc()).all()
AcceptedOrders = Order.query.filter_by(status='accepted').order_by(Order.date_created.asc()).all()
return render_template('admin/list_orders.html', neworders=NewOrders, oldorders=AcceptedOrders)
@admin.route("/listdeployments", methods=['GET'])
@fresh_login_required
@admin_required
def list_deployments():
AllDeploymentsProtected = Deployment.query.filter_by(deleted=False).order_by(Deployment.daysleft.asc()).all()
statuses = {}
for deploy in AllDeploymentsProtected:
data = { 'unit_id': int(deploy.machine_id),
'type': 'kvm' }
try:
query = contact_proxmaster(data, 'status')
status = { int(deploy.machine_id): str(query['status']) }
statuses.update(status)
except:
pass
return render_template('admin/list_deployments.html', deployments=AllDeploymentsProtected, status=statuses)
@admin.route("/listservices", methods=['GET'])
@fresh_login_required
@admin_required
def list_services():
allservices = Service.query.filter_by(deleted=False).order_by(Service.daysleft.asc()).all()
return render_template('admin/list_services.html', services=allservices)
@admin.route("/listdomains", methods=['GET'])
@fresh_login_required
@admin_required
def list_domains():
alldomains = Domain.query.filter_by(deleted=False).order_by(Domain.daysleft.asc()).all()
return render_template('admin/list_domains.html', domains=alldomains)
@admin.route("/listarchive", methods=['GET'])
@fresh_login_required
@admin_required
def list_archive():
deployments = Deployment.query.filter_by(protected=False).order_by(Deployment.daysleft.asc()).all()
services = Service.query.filter_by(deleted=True).all()
domains = Domain.query.filter_by(deleted=True).all()
return render_template('admin/list_archive.html', deployments=deployments, services=services, domains=domains)
@admin.route("/listusers/", defaults={'page': 1})
@admin.route("/listusers/<int:page>", methods=['GET'])
@fresh_login_required
@admin_required
def list_users(page):
sqlquery = User.query.filter_by(active=True).order_by(User.last_seen.desc()).paginate(page, current_app.config['ITEMS_PER_PAGE'], error_out=False)
return render_template('admin/list_users.html', users=sqlquery.items, page=page)
@admin.route("/charge/<int:user_pid>", methods=['GET', 'POST'])
@fresh_login_required
@admin_required
def charge(user_pid=0):
cuser = User.query.filter_by(pid=user_pid).first()
form = ChargeForm()
if form.validate_on_submit():
transaction = Transaction(user_id=int(cuser.pid), description='Account charged by staff', value=float(form.amount.data))
db.session.add(transaction)
db.session.commit()
cuser.wallet += float(form.amount.data)
db.session.add(cuser)
db.session.commit()
return redirect(url_for('admin.list_users'))
return render_template('admin/charge.html', form=form, usr=cuser)
@admin.route("/listaddresses", methods=['GET'])
@fresh_login_required
@admin_required
def list_addresses():
alladdresses = Address.query.all()
alladdrlist = []
for addr in alladdresses:
alladdrlist.append(addr.ip)
ipobjs = sorted(ipaddress.ip_address(addr) for addr in alladdrlist)
ipnrml = []
for ipobj in ipobjs:
ipnrml.append(str(ipobj))
alladdr = sorted(alladdresses, key=lambda o: ipnrml.index(o.ip))
return render_template('admin/list_addresses.html', addresses=alladdr)
@admin.route("/addr2pool", methods=['GET', 'POST'])
@fresh_login_required
@admin_required
def addr2pool():
alladdrlist = []
alladdr = Address.query.all()
for addr in alladdr:
alladdrlist.append(str(addr.ip))
#current_app.logger.info('Current IP pool: {}'.format(alladdrlist))
form = Addr2PoolForm()
if form.validate_on_submit():
if form.ip.data in alladdrlist:
flash('IP address {} is already in the pool!'.format(form.ip.data))
return redirect(url_for('admin.addr2pool'))
address = Address(ip=form.ip.data, rdns=form.rdns.data, region_id=form.region.data, enabled=True, reserved=form.reserved.data)
db.session.add(address)
db.session.commit()
flash('Address {} added to region {}'.format(form.ip.data, form.region.data))
return redirect(url_for('admin.addr2pool'))
return render_template('admin/addr2pool.html', form=form, alladdresses=alladdrlist)
@admin.route("/listservers", methods=['GET'])
@fresh_login_required
@admin_required
def list_servers():
allservers = Server.query.all()
return render_template('admin/list_servers.html', servers=allservers)
@admin.route("/listtransactions/", defaults={'page': 1})
@admin.route("/listtransactions/<int:page>", methods=['GET'])
@fresh_login_required
@admin_required
def list_transactions(page):
sqlquery = Transaction.query.order_by(Transaction.date_created.desc()).paginate(page, current_app.config['ITEMS_PER_PAGE'], error_out=False)
return render_template('admin/list_transactions.html', transactions=sqlquery.items, page=page)
@admin.route("/transaction/<int:user_pid>", methods=['GET'])
@fresh_login_required
@admin_required
def transaction(user_pid=0):
cuser = User.query.filter_by(pid=user_pid).first()
transactions = cuser.inv_transactions.order_by(Transaction.date_created.desc()).limit(20)
labelslist = ['today']
translist = [cuser.wallet]
prevvalue = cuser.wallet
for tr in transactions:
labelslist.insert(0, str(tr.date_created.strftime('%d.%m')))
translist.insert(0, prevvalue - tr.value)
prevvalue -= tr.value
if len(labelslist) <= 1:
labelslist.insert(0, 'before')
translist.insert(0, 0)
#current_app.logger.info('[{}] transactions: {} {} '.format(cuser.email, translist, labelslist))
return render_template('uinvoice/transactions.html', transactions=transactions, translist=translist, labelslist=labelslist, cuser=cuser)

View file

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

View file

@ -0,0 +1,51 @@
from flask_wtf import FlaskForm, RecaptchaField
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
from wtforms import validators, ValidationError
from wtforms.fields.html5 import EmailField
from ..models import User
class LoginForm(FlaskForm):
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
password = PasswordField('Password', [validators.DataRequired(), validators.Length(1,128)])
remember_me = BooleanField('Remember me ?')
#recaptcha = RecaptchaField()
submit = SubmitField('Login')
class TwoFAForm(FlaskForm):
token = StringField('Token', [validators.DataRequired(), validators.Length(6, 6)])
submit = SubmitField('Confirm')
class RegistrationForm(FlaskForm):
email = StringField('E-Mail', [validators.DataRequired(), validators.Length(6,35), validators.Email()])
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Error. Please try again.')
password = PasswordField('Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both passwords must be equal')])
confirm = PasswordField('Your password again', [validators.DataRequired()])
accept_tos = BooleanField('I accept the <a href="/terms">Terms of Service</a>', [validators.DataRequired()])
recaptcha = RecaptchaField()
submit = SubmitField('REGISTER')
class ChangePasswordForm(FlaskForm):
old_password = PasswordField('Old Password', [validators.DataRequired()])
password = PasswordField('New Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both passwords must be equal')])
confirm = PasswordField('Your password again')
submit = SubmitField('Renew Password')
class PasswordResetRequestForm(FlaskForm):
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
recaptcha = RecaptchaField()
submit = SubmitField('Reset password', [validators.DataRequired()])
class PasswordResetForm(FlaskForm):
email = EmailField('E-Mail', [validators.DataRequired(), validators.Length(1,64), validators.Email()])
password = PasswordField('Password', [validators.DataRequired(), validators.EqualTo('confirm', message='Both password fields must be equal')])
confirm = PasswordField('Your password again', [validators.DataRequired()])
submit = SubmitField('Change password')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Error. Please try again.')

234
flask/forest/auth/routes.py Normal file
View file

@ -0,0 +1,234 @@
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, Transaction
from ..email import send_email
from .forms import LoginForm, TwoFAForm, RegistrationForm, ChangePasswordForm,PasswordResetRequestForm, PasswordResetForm
from ..decorators import admin_required, permission_required
from io import BytesIO
import pyqrcode
def get_google_auth(state=None, token=None):
if token:
return OAuth2Session(current_app.config['CLIENT_ID'], token=token)
if state:
return OAuth2Session(
current_app.config['CLIENT_ID'],
state=state,
redirect_uri=current_app.config['REDIRECT_URI'])
oauth = OAuth2Session(
current_app.config['CLIENT_ID'],
redirect_uri=current_app.config['REDIRECT_URI'],
scope=current_app.config['SCOPE'])
return oauth
@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('main.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.active == False:
flash('User disabled.')
return redirect(url_for('main.index'))
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)
previp = user.last_ip
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 )
#flash('Last Login: {} from {}'.format(user.last_seen.strftime("%a %d %B %Y %H:%M"), previp))
flash('Last Login: {}'.format(user.last_seen.strftime("%a %d %B %Y %H:%M")))
return redirect(request.args.get('next') or url_for('panel.dashboard'))
else:
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('panel.dashboard'))
else:
flash('Invalid token.')
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('main.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, wallet=current_app.config['REGISTER_BONUS'])
db.session.add(user)
db.session.commit()
#transaction = Transaction(user_id=int(user.pid), description='Registered account bonus', value=current_app.config['REGISTER_BONUS'])
#db.session.add(transaction)
#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('main.index'))
if current_user.confirm(token):
flash('Вашият акаунт е потвърден. Благодаря!')
else:
flash('Времето за потвърждение на вашият код изтече.')
return redirect(url_for('main.index'))
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm_your_account',
'auth/email/confirm', user=current_user, token=token)
flash('New confirmation code was sent.')
return redirect(url_for('main.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('Your password was changed')
return redirect(url_for('main.index'))
else:
flash('Wrong password.')
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('main.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('main.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('main.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('main.index'))
return render_template('auth/reset_password.html', form=form)

View file

@ -0,0 +1,25 @@
from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission
from threading import Thread
def async(f):
def wrapper(*args, **kwargs):
thr = Thread(target=f, args=args, kwargs=kwargs)
thr.start()
return wrapper
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)

24
flask/forest/email.py Normal file
View file

@ -0,0 +1,24 @@
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import app, mail
from .decorators import async
@async
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
if len(subject) > 50:
newsubject = subject[:50] + '...'
else:
newsubject = subject
app = current_app._get_current_object()
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + ' ' + newsubject, 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

View file

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

57
flask/forest/init.py Normal file
View file

@ -0,0 +1,57 @@
import os
from werkzeug.utils import secure_filename
from flask import (
Flask,
jsonify,
send_from_directory,
request,
redirect,
url_for
)
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object("project.config.Config")
db = SQLAlchemy(app)
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(128), unique=True, nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
def __init__(self, email):
self.email = email
@app.route("/")
def hello_world():
return jsonify(hello="world")
@app.route("/static/<path:filename>")
def staticfiles(filename):
return send_from_directory(app.config["STATIC_FOLDER"], filename)
@app.route("/media/<path:filename>")
def mediafiles(filename):
return send_from_directory(app.config["MEDIA_FOLDER"], filename)
@app.route("/upload", methods=["GET", "POST"])
def upload_file():
if request.method == "POST":
file = request.files["file"]
filename = secure_filename(file.filename)
file.save(os.path.join(app.config["MEDIA_FOLDER"], filename))
return """
<!doctype html>
<title>upload new File</title>
<form action="" method=post enctype=multipart/form-data>
<p><input type=file name=file><input type=submit value=Upload>
</form>
"""

211
flask/forest/models.py Normal file
View file

@ -0,0 +1,211 @@
# FAT MODEL
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
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 random
import uuid
import base64
import hashlib
import json
from datetime import date, time, datetime, timedelta
import onetimepass
class Permission:
DEPLOY = 0x01
ADMINISTER = 0xff
class Role(db.Model):
__tablename__ = 'roles'
pid = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, 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': (Permission.ADMINISTER, 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'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(128), unique=True, nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
confirmed = db.Column(db.Boolean, default=False)
role_id = db.Column(db.ForeignKey('roles.pid')) #FK
password_hash = db.Column(db.String)
tokens = db.Column(db.Text)
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
last_ip = db.Column(db.String)
twofactor = db.Column(db.Boolean, default=False) #optional 2factor auth
otp_secret = db.Column(db.String)
avatar_hash = db.Column(db.String)
uuid = db.Column(db.String)
name = db.Column(db.Unicode)
address = db.Column(db.Unicode)
city = db.Column(db.Unicode)
postcode = db.Column(db.String)
country = db.Column(db.String, default='BG')
phone = db.Column(db.String)
group = db.Column(db.String, default='User')
language = db.Column(db.String, default='BG')
wallet = db.Column(db.Float)
currency = db.Column(db.String, default='BGN')
inv_items = db.relationship('Item', backref='owner', lazy='dynamic')
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.avatar_hash is None and self.email is not 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')
if self.uuid is None:
# generate uuid
self.uuid = uuid.uuid4()
@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):
if self.can(Permission.ADMINISTER):
return True
else:
return False
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))
#ITEMS
class Item(db.Model):
__tablename__ = 'items'
id = db.Column(db.Integer, primary_key=True) #PK
user_id = db.Column(db.ForeignKey('users.id')) #FK
date_created = db.Column(db.DateTime, default=datetime.utcnow)
description = db.Column(db.Unicode)

View file

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

View file

@ -0,0 +1,27 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField, SelectField, DecimalField
from flask_pagedown.fields import PageDownField
from wtforms import validators, ValidationError
from wtforms.fields.html5 import EmailField, DecimalRangeField
from .. import db
class OrderForm(FlaskForm):
region_choices = [(1, 'Plovdiv, Bulgaria'), (2, 'International Space Station')]
region = SelectField('Region:', choices=region_choices, coerce=int)
recipe_choices = [(1, 'RootVPS')]
recipe = SelectField('Type:', choices=recipe_choices, coerce=int)
cpu = DecimalRangeField('Processor Cores', default=2)
memory = DecimalRangeField('Memory', default=2048)
storage = DecimalRangeField('Storage', default=20)
alias = StringField('Name:', [validators.Regexp(message='ex.: 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})$'), validators.Length(6,64)])
submit = SubmitField('DEPLOY')
class MessageForm(FlaskForm):
line = PageDownField('Enter your message...', validators=[validators.DataRequired()])
submit = SubmitField('Submit')

View file

@ -0,0 +1,165 @@
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 panel
from .forms import OrderForm, MessageForm
from .. import db
from ..email import send_email
from ..models import User, Permission, Recipe, Order, Server, Deployment, Service, Region, Address, Domain, SupportTopic, SupportLine, contact_proxmaster
import base64
from datetime import date, time, datetime
from dateutil.relativedelta import relativedelta
@panel.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
@panel.route("/deploy", methods=['GET', 'POST'])
@login_required
def deploy():
if current_user.name is None:
return redirect(url_for('settings.profile'))
form = OrderForm()
if form.validate_on_submit():
region = Region.query.filter_by(pid=int(form.region.data)).first()
recipe = Recipe.query.filter_by(pid=int(form.recipe.data)).first()
new_order = Order(user_id=int(current_user.pid), region_id=int(region.pid), recipe_id=int(recipe.pid), parameter1=str(form.alias.data), parameter2=str(form.cpu.data), parameter3=str(form.memory.data), parameter4=str(form.storage.data), status='new')
db.session.add(new_order)
db.session.commit()
send_email(current_app.config['MAIL_USERNAME'], 'New order from {}'.format(current_user.email),
'panel/email/adm_neworder', user=current_user)
return redirect(request.args.get('next') or url_for('panel.dashboard'))
return render_template('panel/deploy.html', form=form)
#DASHBOARD
@panel.route("/dashboard", defaults={'user_pid': 0}, methods=['GET'])
@panel.route("/dashboard/<int:user_pid>", methods=['GET'])
@login_required
def dashboard(user_pid):
sys_regions = Region.query.all()
if user_pid == 0:
cuser = current_user
else:
cuser = User.query.filter_by(pid=user_pid).first()
if cuser == None:
abort(404)
if not current_user.is_administrator():
abort(404) #hidden 403
inv_addresses = cuser.inv_addresses.order_by(Address.ip.asc()).all()
inv_deployments = cuser.inv_deployments.filter_by(deleted=False).order_by(Deployment.machine_alias.asc()).all()
regions = {}
for region in sys_regions:
regions[region.pid] = region.description
inv_deploycubeids = []
warnflag = False
for invcls in inv_deployments:
if invcls.user_id == cuser.pid:
inv_deploycubeids.extend([invcls.machine_id])
#warning detector
if invcls.warning == True or invcls.enabled == False:
warnflag = True
inv_services = cuser.inv_services.filter_by(deleted=False).order_by(Service.date_last_charge.asc()).all()
inv_domains = cuser.inv_domains.filter_by(deleted=False).order_by(Domain.date_created.desc()).all()
#extract rrd and status from the deployments
rrd = {}
statuses = {}
#current_app.logger.warning(str(inv_deploycubeids))
for unit_id in inv_deploycubeids:
data = { 'unit_id': int(unit_id),
'type': 'kvm' }
try:
query = contact_proxmaster(data, 'vmrrd')
graphs_list = ['net', 'cpu', 'mem', 'hdd']
rrd[unit_id] = {}
for graph in graphs_list:
raw = query[graph]['image'].encode('raw_unicode_escape')
rrd[unit_id][graph] = base64.b64encode(raw).decode()
status = { unit_id : query['status'] }
statuses.update(status)
except Exception as e:
current_app.logger.error(e)
for invcls in inv_deployments:
if invcls.machine_id == unit_id:
inv_deployments.remove(invcls)
flash('Support is notified about {}.'.format(str(cuser.inv_deployments.filter_by(machine_id=unit_id).first().machine_alias)))
if not current_user.is_administrator():
send_email(current_app.config['MAIL_USERNAME'], '{} experienced an error'.format(cuser.email), 'vmanager/email/adm_unreachable', user=current_user, unit_id=unit_id, error=repr(e))
continue
supportform = MessageForm()
return render_template('panel/dashboard.html', sys_regions=sys_regions, inv_deployments=inv_deployments, inv_services=inv_services, inv_domains=inv_domains, inv_addresses=inv_addresses, rrd=rrd, status=statuses, warnflag=warnflag, regions=regions, form=supportform)
#SUPPORT
@panel.route("/list", methods=['GET'])
@login_required
def support_list():
""" general enquiry and list all open support tasks """
cuser = current_user
form = MessageForm()
alltopics = cuser.inv_topics.all()
return render_template('panel/support_list.html', form=form, inv_topics=alltopics)
@panel.route("/topic/<string:topic>/", methods=['GET', 'POST'])
@login_required
def support(topic):
""" block item for support chatbox. invoked from vdc_pool or supportlist """
cuser = current_user
form = MessageForm()
if request.method == "GET":
support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first()
if support_topic == None:
class EmptySupport():
hashtag=str(topic)
timestamp=datetime.utcnow()
support_topic = EmptySupport()
return render_template('panel/support_item.html', form=form, support=support_topic)
else:
if support_topic.user_id != cuser.pid:
abort(403) #TODO: hidden 403. there is a topic like that but its not yours!
else:
#topic is yours. show it.
return render_template('panel/support_item.html', form=form, support=support_topic)
if request.method == "POST" and form.validate_on_submit():
support_topic = SupportTopic.query.filter_by(hashtag=str(topic)).first()
if support_topic == None:
#no topic. create one?
if cuser.inv_topics.all() != []:
#check if other topics exist, and ratelimit
last_topic = cuser.inv_topics.order_by(SupportTopic.timestamp.desc()).first()
now = datetime.utcnow()
time_last_topic = last_topic.timestamp
expiry = time_last_topic + relativedelta(time_last_topic, minutes=+5)
if now < expiry:
flash('ratelimit. try again later')
return redirect(url_for('panel.support_list'))
#create new topic
new_topic = SupportTopic(user_id=cuser.pid, hashtag=str(topic))
db.session.add(new_topic)
new_line = SupportLine(topic_id=new_topic.pid, line=str(form.line.data))
db.session.add(new_line)
else:
if support_topic.user_id == cuser.pid:
new_line = SupportLine(topic_id=support_topic.pid, line=form.line.data)
db.session.add(new_line)
else:
abort(403) #TODO: hidden 404
db.session.commit()
return redirect(url_for('panel.support_list'))

View file

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

View file

@ -0,0 +1,66 @@
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('Name:', [validators.DataRequired(), validators.Length(3, 60)])
address = StringField('Address:', [validators.DataRequired(), validators.Length(2, 50)])
city = StringField('City:', [validators.DataRequired(), validators.Length(2,40)])
postcode = StringField('Postcode:')
clist = []
for c in countries:
clist.append((c.alpha2, c.name))
country = SelectField('Country:', choices=clist, default='BG')
phone = StringField('Phone:')
org_account = BooleanField('This is a business account.')
org_companyname = StringField('Company Name:')
org_regaddress = StringField('Company Address:')
org_responsible = StringField('Accountable Person:')
org_vatnum = StringField('VAT Number:')
twofactor = BooleanField('Enable 2-factor authentication')
submit = SubmitField('Update')
class EditProfileAdminForm(FlaskForm):
email = StringField('Електроннa поща (логин):', [validators.DataRequired(), validators.Length(1, 64), validators.Email()])
confirmed = BooleanField('Activated')
role = SelectField('Role', 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_account = BooleanField('This is a business account')
org_companyname = StringField('Company Name:')
org_regaddress = StringField('Company Address:')
org_responsible = StringField('Primary Contact:')
org_vatnum = StringField('ДДС Номер:')
twofactor = BooleanField('2-factor authentication')
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-а е вече регистриран.')

View file

@ -0,0 +1,53 @@
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 settings
from .forms import EditProfileForm, EditProfileAdminForm
from ..email import send_email
from .. import db
from ..models import User
import sys
#PROFILE
@settings.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
page = { 'title': 'Edit Profile' }
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_account = form.org_account.data
current_user.org_companyname = form.org_companyname.data
current_user.org_regaddress = form.org_regaddress.data
current_user.org_responsible = form.org_responsible.data
current_user.org_vatnum = form.org_vatnum.data
current_user.twofactor = form.twofactor.data
db.session.add(current_user)
db.session.commit()
flash('Info Updated!')
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_account.data = current_user.org_account
form.org_companyname.data = current_user.org_companyname
form.org_regaddress.data = current_user.org_regaddress
form.org_responsible.data = current_user.org_responsible
form.org_vatnum.data = current_user.org_vatnum
form.twofactor.data = current_user.twofactor
wallet = "%.2f" % round(current_user.wallet, 3)
#current_app.logger.info('[{}] wallet: {}'.format(current_user.email, wallet))
return render_template('settings/profile.html', page=page, form=form, wallet=wallet, cuser=current_user)

View file

@ -0,0 +1,277 @@
/*! =======================================================
VERSION 9.4.1
========================================================= */
/*! =========================================================
* bootstrap-slider.js
*
* Maintainers:
* Kyle Kemp
* - Twitter: @seiyria
* - Github: seiyria
* Rohit Kalkur
* - Twitter: @Rovolutionary
* - Github: rovolution
*
* =========================================================
*
* bootstrap-slider is released under the MIT License
* Copyright (c) 2016 Kyle Kemp, Rohit Kalkur, and contributors
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* ========================================================= */
.slider {
display: inline-block;
vertical-align: middle;
position: relative;
}
.slider.slider-horizontal {
width: 210px;
height: 20px;
}
.slider.slider-horizontal .slider-track {
height: 10px;
width: 100%;
margin-top: -5px;
top: 50%;
left: 0;
}
.slider.slider-horizontal .slider-selection,
.slider.slider-horizontal .slider-track-low,
.slider.slider-horizontal .slider-track-high {
height: 100%;
top: 0;
bottom: 0;
}
.slider.slider-horizontal .slider-tick,
.slider.slider-horizontal .slider-handle {
margin-left: -10px;
}
.slider.slider-horizontal .slider-tick.triangle,
.slider.slider-horizontal .slider-handle.triangle {
position: relative;
top: 50%;
transform: translateY(-50%);
border-width: 0 10px 10px 10px;
width: 0;
height: 0;
border-bottom-color: #0480be;
margin-top: 0;
}
.slider.slider-horizontal .slider-tick-container {
white-space: nowrap;
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.slider.slider-horizontal .slider-tick-label-container {
white-space: nowrap;
margin-top: 20px;
}
.slider.slider-horizontal .slider-tick-label-container .slider-tick-label {
padding-top: 4px;
display: inline-block;
text-align: center;
}
.slider.slider-vertical {
height: 210px;
width: 20px;
}
.slider.slider-vertical .slider-track {
width: 10px;
height: 100%;
left: 25%;
top: 0;
}
.slider.slider-vertical .slider-selection {
width: 100%;
left: 0;
top: 0;
bottom: 0;
}
.slider.slider-vertical .slider-track-low,
.slider.slider-vertical .slider-track-high {
width: 100%;
left: 0;
right: 0;
}
.slider.slider-vertical .slider-tick,
.slider.slider-vertical .slider-handle {
margin-top: -10px;
}
.slider.slider-vertical .slider-tick.triangle,
.slider.slider-vertical .slider-handle.triangle {
border-width: 10px 0 10px 10px;
width: 1px;
height: 1px;
border-left-color: #0480be;
margin-left: 0;
}
.slider.slider-vertical .slider-tick-label-container {
white-space: nowrap;
}
.slider.slider-vertical .slider-tick-label-container .slider-tick-label {
padding-left: 4px;
}
.slider.slider-disabled .slider-handle {
background-image: -webkit-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
background-image: -o-linear-gradient(top, #dfdfdf 0%, #bebebe 100%);
background-image: linear-gradient(to bottom, #dfdfdf 0%, #bebebe 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf', endColorstr='#ffbebebe', GradientType=0);
}
.slider.slider-disabled .slider-track {
background-image: -webkit-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
background-image: -o-linear-gradient(top, #e5e5e5 0%, #e9e9e9 100%);
background-image: linear-gradient(to bottom, #e5e5e5 0%, #e9e9e9 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5', endColorstr='#ffe9e9e9', GradientType=0);
cursor: not-allowed;
}
.slider input {
display: none;
}
.slider .tooltip.top {
margin-top: -36px;
}
.slider .tooltip-inner {
white-space: nowrap;
max-width: none;
}
.slider .hide {
display: none;
}
.slider-track {
position: absolute;
cursor: pointer;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #f9f9f9 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #f9f9f9 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.slider-selection {
position: absolute;
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-radius: 4px;
}
.slider-selection.tick-slider-selection {
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
}
.slider-track-low,
.slider-track-high {
position: absolute;
background: transparent;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-radius: 4px;
}
.slider-handle {
position: absolute;
top: 0;
width: 20px;
height: 20px;
background-color: #337ab7;
background-image: -webkit-linear-gradient(top, #149bdf 0%, #0480be 100%);
background-image: -o-linear-gradient(top, #149bdf 0%, #0480be 100%);
background-image: linear-gradient(to bottom, #149bdf 0%, #0480be 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);
filter: none;
-webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
border: 0px solid transparent;
}
.slider-handle.round {
border-radius: 50%;
}
.slider-handle.triangle {
background: transparent none;
}
.slider-handle.custom {
background: transparent none;
}
.slider-handle.custom::before {
line-height: 20px;
font-size: 20px;
content: '\2605';
color: #726204;
}
.slider-tick {
position: absolute;
width: 20px;
height: 20px;
background-image: -webkit-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #f9f9f9 0%, #f5f5f5 100%);
background-image: linear-gradient(to bottom, #f9f9f9 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9', endColorstr='#fff5f5f5', GradientType=0);
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
filter: none;
opacity: 0.8;
border: 0px solid transparent;
}
.slider-tick.round {
border-radius: 50%;
}
.slider-tick.triangle {
background: transparent none;
}
.slider-tick.custom {
background: transparent none;
}
.slider-tick.custom::before {
line-height: 20px;
font-size: 20px;
content: '\2605';
color: #726204;
}
.slider-tick.in-selection {
background-image: -webkit-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: -o-linear-gradient(top, #89cdef 0%, #81bfde 100%);
background-image: linear-gradient(to bottom, #89cdef 0%, #81bfde 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef', endColorstr='#ff81bfde', GradientType=0);
opacity: 1;
}

View file

@ -0,0 +1,57 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 20px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 12px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: #4CAF50;
width: 100%;
border: 0;
padding: 15px;
color: #FFFFFF;
font-size: 16px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,.form button:active,.form button:focus {
background: #43A047;
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: #4CAF50;
text-decoration: none;
}
.form .register-form {
display: none;
}

View file

@ -0,0 +1,90 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
.login-page {
width: 360px;
padding: 8% 0 0;
margin: auto;
}
.form {
position: relative;
z-index: 1;
background: #FFFFFF;
max-width: 360px;
margin: 0 auto 100px;
padding: 45px;
text-align: center;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
}
.form input {
font-family: "Roboto", sans-serif;
outline: 0;
background: #f2f2f2;
width: 100%;
border: 0;
margin: 0 0 15px;
padding: 15px;
box-sizing: border-box;
font-size: 14px;
}
.form button {
font-family: "Roboto", sans-serif;
text-transform: uppercase;
outline: 0;
background: #4CAF50;
width: 100%;
border: 0;
padding: 15px;
color: #FFFFFF;
font-size: 14px;
-webkit-transition: all 0.3 ease;
transition: all 0.3 ease;
cursor: pointer;
}
.form button:hover,.form button:active,.form button:focus {
background: #43A047;
}
.form .message {
margin: 15px 0 0;
color: #b3b3b3;
font-size: 12px;
}
.form .message a {
color: #4CAF50;
text-decoration: none;
}
.form .register-form {
display: none;
}
.container {
position: relative;
z-index: 1;
max-width: 300px;
margin: 0 auto;
}
.container:before, .container:after {
content: "";
display: block;
clear: both;
}
.container .info {
margin: 50px auto;
text-align: center;
}
.container .info h1 {
margin: 0 0 15px;
padding: 0;
font-size: 36px;
font-weight: 300;
color: #1a1a1a;
}
.container .info span {
color: #4d4d4d;
font-size: 12px;
}
.container .info span a {
color: #000000;
text-decoration: none;
}
.container .info span .fa {
color: #EF3B3A;
}

View file

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

View file

@ -0,0 +1,53 @@
.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}
@media only screen and (max-width: 768px) {
/* Force table to not be like tables anymore */
.no-more-tables table,
.no-more-tables thead,
.no-more-tables tbody,
.no-more-tables th,
.no-more-tables td,
.no-more-tables tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
.no-more-tables thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
.no-more-tables tr { border: 1px solid #ccc; }
.no-more-tables td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50%;
white-space: normal;
text-align:left;
}
.no-more-tables td:before {
/* Now like a table header */
/* position: absolute; */
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
}
/*
Label the data
*/
.no-more-tables td:before { content: attr(data-title); }
}

View file

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

View file

@ -0,0 +1,13 @@
.panel-transparent {
background: none;
}
.panel-transparent .panel-heading {
background: rgb(255, 255, 255); /* fallback */
background: rgba(255, 255, 255, 0.6)!important;
}
.panel-transparent .panel-body{
background: rgb(255, 255, 255); /* fallback */
background: rgba(255, 255, 255, 0.5)!important;
}

View file

@ -0,0 +1,86 @@
input[type=range] {
-webkit-appearance: none;
width: 100%;
margin: 13.8px 0;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-webkit-slider-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #f8ffff;
cursor: pointer;
-webkit-appearance: none;
margin-top: -14px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #367ebd;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 8.4px;
cursor: pointer;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
background: #3071a9;
border-radius: 1.3px;
border: 0.2px solid #010101;
}
input[type=range]::-moz-range-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #f8ffff;
cursor: pointer;
}
input[type=range]::-ms-track {
width: 100%;
height: 8.4px;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower {
background: #2a6495;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-fill-upper {
background: #3071a9;
border: 0.2px solid #010101;
border-radius: 2.6px;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
}
input[type=range]::-ms-thumb {
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
border: 1px solid #000000;
height: 36px;
width: 16px;
border-radius: 3px;
background: #f8ffff;
cursor: pointer;
height: 8.4px;
}
input[type=range]:focus::-ms-fill-lower {
background: #3071a9;
}
input[type=range]:focus::-ms-fill-upper {
background: #367ebd;
}

View file

@ -0,0 +1,138 @@
.bss-slides{
position: relative;
display: block;
}
.bss-slides:focus{
outline: 0;
}
.bss-slides figure{
position: absolute;
top: 0;
width: 100%;
}
.bss-slides figure:first-child{
position: relative;
}
.bss-slides figure img{
opacity: 0;
-webkit-transition: opacity 1.2s;
transition: opacity 1.2s;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.bss-slides .bss-show{
z-index: 2;
}
.bss-slides .bss-show img{
opacity: 1;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
position: relative;
}
.bss-slides figcaption{
position: absolute;
font-family: sans-serif;
font-size: .8em;
bottom: .75em;
right: .35em;
padding: .25em;
color: #fff;
background: #000;
background: rgba(0,0,0, .25);
border-radius: 2px;
opacity: 0;
-webkit-transition: opacity 1.2s;
transition: opacity 1.2s;
}
.bss-slides .bss-show figcaption{
z-index: 3;
opacity: 1;
}
.bss-slides figcaption a{
color: #fff;
}
.bss-next, .bss-prev{
visibility: hidden;
color: #fff;
position: absolute;
background: #000;
background: rgba(0,0,0, .6);
top: 50%;
z-index: 4;
font-size: 2em;
margin-top: -1.2em;
opacity: .3;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.bss-next:hover, .bss-prev:hover{
cursor: pointer;
opacity: 1;
}
.bss-next{
right: -1px;
padding: 10px 5px 15px 10px;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.bss-prev{
left: 0;
padding: 10px 10px 15px 5px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
.bss-fullscreen{
display: block;
width: 32px;
height: 32px;
background: rgba(0,0,0,.4) url(/static/images/arrows-alt_ffffff_64.png);
-webkit-background-size: contain;
background-size: contain;
position: absolute;
top: 5px;
left: 5px;
cursor: pointer;
opacity: .3;
}
.bss-fullscreen:hover{
opacity: .8;
}
:-webkit-full-screen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
-webkit-background-size: contain;
background-size: contain;
}
:-moz-full-screen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
background-size: contain;
}
:-ms-fullscreen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
background-size: contain;
}
:full-screen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
-webkit-background-size: contain;
background-size: contain;
}
:-webkit-full-screen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
-webkit-background-size: contain;
background-size: contain;
}
:-moz-full-screen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
background-size: contain;
}
:-ms-fullscreen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
background-size: contain;
}
:fullscreen .bss-fullscreen{
background: rgba(0,0,0,.4) url(/static/images/compress_ffffff_64.png);
-webkit-background-size: contain;
background-size: contain;
}

View file

@ -0,0 +1,295 @@
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
body {
/* background: url('/static/images/purplebg.jpg') no-repeat center center fixed; */
background-size: cover;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
padding-top: 0px;
background-color: #edf0f5;
background-repeat: no-repeat;
background-attachment: fixed;
top:0;
left:0;
width:100%;
height:100%;
font-size: 16px;
/* font-weight: bold; */
font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page_wrap {
width: 88%;
margin: 0 auto;
}
.container {
width: 90%;
}
.container-fluid {
max-width: 92%;
min-width: 280px;
}
.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%;
}
#footer_cols {
background:url('/static/images/footer_header_bg.png') repeat;
border-bottom: 1px solid #2c2c2e;
overflow: hidden;
padding: 16px 0 20px 0;
color: #818387;
margin-top: 40px;
}
.clear {
clear:both;
}
.footerblock {
max-width: 420px;
margin-right: 0px;
}
.footerblock h4 {
font-size: 18px;
color: #FFF;
background: url('/static/images/footer_cols_divider.png') no-repeat bottom center;
padding-bottom: 13px;
margin-bottom: 8px;
}
.last_col {
margin-right: 0;
}
.tweet_day {
font-size: 14px;
color: #484a4e;
font-style: italic;
}
.form_field {
position: relative;
}
.newsletter_input {
background: url('/static/images/footer_newsletter_input.png') no-repeat;
width: 190px;
height: 38px;
padding: 0 10px;
border: 0;
outline: 0;
font-size: 13px;
color: #666666;
font-weight: bold;
}
.newsletter_submit {
background: url('/static/images/footer_newsletter_input_btn.png') no-repeat;
width: 16px;
height: 16px;
border: 0;
text-indent: -9999px;
font-size: 0;
position: absolute;
right: 10px;
top: 10px;
cursor: pointer;
}
/* Footer Copyright */
#footer_copyright {
background-color: #536a2f;
/* background: url(/static/images/footer_top_bg.png) repeat; */
overflow: hidden;
padding: 20px 0 5px 0;
}
.copyright {
color: #fff;
float: left;
width: 500px;
}
.copyright a {
color: #fff;
}
.design_by {
color: #fff;
float: right;
width: 300px;
text-align: right;
}
.design_by a {
color: #fff;
}
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: 660px;
}
.padding-left-32 {
padding-left: 32px;
}
.padding-left-16 {
padding-left: 16px;
}
.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;
}
.panel-heading {
padding: 6px 15px;
border-bottom: 1px solid transparent;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.rrd + .tooltip > .tooltip-inner {
background-color: #73AD21;
color: #FFFFFF;
border: 1px solid green;
padding: 15px;
font-size: 20px;
}
/* Tooltip on top */
.rrd + .tooltip.top > .tooltip-arrow {
border-top: 5px solid green;
}
/* Tooltip on bottom */
.rrd + .tooltip.bottom > .tooltip-arrow {
border-bottom: 5px solid blue;
}
/* Tooltip on left */
.rrd + .tooltip.left > .tooltip-arrow {
border-left: 5px solid red;
}
/* Tooltip on right */
.rrd + .tooltip.right > .tooltip-arrow {
border-right: 5px solid black;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 B

View file

@ -0,0 +1,163 @@
var makeBSS = function (el, options) {
var $slideshows = document.querySelectorAll(el), // a collection of all of the slideshow
$slideshow = {},
Slideshow = {
init: function (el, options) {
options = options || {}; // if options object not passed in, then set to empty object
options.auto = options.auto || false; // if options.auto object not passed in, then set to false
this.opts = {
selector: (typeof options.selector === "undefined") ? "figure" : options.selector,
auto: (typeof options.auto === "undefined") ? false : options.auto,
speed: (typeof options.auto.speed === "undefined") ? 1500 : options.auto.speed,
pauseOnHover: (typeof options.auto.pauseOnHover === "undefined") ? false : options.auto.pauseOnHover,
fullScreen: (typeof options.fullScreen === "undefined") ? false : options.fullScreen,
swipe: (typeof options.swipe === "undefined") ? false : options.swipe
};
this.counter = 0; // to keep track of current slide
this.el = el; // current slideshow container
this.$items = el.querySelectorAll(this.opts.selector); // a collection of all of the slides, caching for performance
this.numItems = this.$items.length; // total number of slides
this.$items[0].classList.add('bss-show'); // add show class to first figure
this.injectControls(el);
this.addEventListeners(el);
if (this.opts.auto) {
this.autoCycle(this.el, this.opts.speed, this.opts.pauseOnHover);
}
if (this.opts.fullScreen) {
this.addFullScreen(this.el);
}
if (this.opts.swipe) {
this.addSwipe(this.el);
}
},
showCurrent: function (i) {
// increment or decrement this.counter depending on whether i === 1 or i === -1
if (i > 0) {
this.counter = (this.counter + 1 === this.numItems) ? 0 : this.counter + 1;
} else {
this.counter = (this.counter - 1 < 0) ? this.numItems - 1 : this.counter - 1;
}
// remove .show from whichever element currently has it
// http://stackoverflow.com/a/16053538/2006057
[].forEach.call(this.$items, function (el) {
el.classList.remove('bss-show');
});
// add .show to the one item that's supposed to have it
this.$items[this.counter].classList.add('bss-show');
},
injectControls: function (el) {
// build and inject prev/next controls
// first create all the new elements
var spanPrev = document.createElement("span"),
spanNext = document.createElement("span"),
docFrag = document.createDocumentFragment();
// add classes
spanPrev.classList.add('bss-prev');
spanNext.classList.add('bss-next');
// add contents
spanPrev.innerHTML = '&laquo;';
spanNext.innerHTML = '&raquo;';
// append elements to fragment, then append fragment to DOM
docFrag.appendChild(spanPrev);
docFrag.appendChild(spanNext);
el.appendChild(docFrag);
},
addEventListeners: function (el) {
var that = this;
el.querySelector('.bss-next').addEventListener('click', function () {
that.showCurrent(1); // increment & show
}, false);
el.querySelector('.bss-prev').addEventListener('click', function () {
that.showCurrent(-1); // decrement & show
}, false);
el.onkeydown = function (e) {
e = e || window.event;
if (e.keyCode === 37) {
that.showCurrent(-1); // decrement & show
} else if (e.keyCode === 39) {
that.showCurrent(1); // increment & show
}
};
},
autoCycle: function (el, speed, pauseOnHover) {
var that = this,
interval = window.setInterval(function () {
that.showCurrent(1); // increment & show
}, speed);
if (pauseOnHover) {
el.addEventListener('mouseover', function () {
interval = clearInterval(interval);
}, false);
el.addEventListener('mouseout', function () {
interval = window.setInterval(function () {
that.showCurrent(1); // increment & show
}, speed);
}, false);
} // end pauseonhover
},
addFullScreen: function(el){
var that = this,
fsControl = document.createElement("span");
fsControl.classList.add('bss-fullscreen');
el.appendChild(fsControl);
el.querySelector('.bss-fullscreen').addEventListener('click', function () {
that.toggleFullScreen(el);
}, false);
},
addSwipe: function(el){
var that = this,
ht = new Hammer(el);
ht.on('swiperight', function(e) {
that.showCurrent(-1); // decrement & show
});
ht.on('swipeleft', function(e) {
that.showCurrent(1); // increment & show
});
},
toggleFullScreen: function(el){
// https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode
if (!document.fullscreenElement && // alternative standard method
!document.mozFullScreenElement && !document.webkitFullscreenElement &&
!document.msFullscreenElement ) { // current working methods
if (document.documentElement.requestFullscreen) {
el.requestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
el.msRequestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
el.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
el.webkitRequestFullscreen(el.ALLOW_KEYBOARD_INPUT);
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
} // end toggleFullScreen
}; // end Slideshow object .....
// make instances of Slideshow as needed
[].forEach.call($slideshows, function (el) {
$slideshow = Object.create(Slideshow);
$slideshow.init(el, options);
});
};

1807
flask/forest/static/js/bootstrap-slider.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
var canvas;
var ctx;
var background;
var width = 300;
var height = 200;
var cloud;
var cloud_x;
function init() {
canvas = document.getElementById("clouds");
width = canvas.width;
height = canvas.height;
ctx = canvas.getContext("2d");
// init background
background = new Image();
background.src = 'http://silveiraneto.net/wp-content/uploads/2011/06/forest.png';
// init cloud
cloud = new Image();
cloud.src = 'http://silveiraneto.net/wp-content/uploads/2011/06/cloud.png';
cloud.onload = function(){
cloud_x = -cloud.width;
};
return setInterval(main_loop, 10);
}
function update(){
cloud_x += 0.3;
if (cloud_x > width ) {
cloud_x = -cloud.width;
}
}
function draw() {
ctx.drawImage(background,0,0);
ctx.drawImage(cloud, cloud_x, 0);
}
function main_loop() {
draw();
update();
}
init();

4
flask/forest/static/js/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

View file

@ -0,0 +1,65 @@
{% extends "base.html" %}
{% block title %}Add IP address to ip pool{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">Add IP address to the pool</div>
<div class="panel-body">
<form method="POST" action="{{ url_for('admin.addr2pool') }}">
<p>
{{ form.region.label }} {{ form.region }}<br />
{% for error in form.region.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.ip.label }} {{ form.ip }}<br />
{% for error in form.ip.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.rdns.label }} {{ form.rdns }}<br />
{% for error in form.rdns.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.reserved.label }} {{ form.reserved }}<br />
{% for error in form.reserved.errors %}
{{ error }}<br />
{% endfor %}
</p>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
</div>
</div>
</div>
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">Current IP Pool</div>
<div class="panel-body">
{% for addr in alladdresses %}
{{ addr }}<br />
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,9 @@
<div class="col-md-12">
<div class="panel panel-warning" id="prxadmincloud">
<div class="panel-heading">Admin Panel</div>
<div class="panel-body">
{% include "admin/menu_cloud.html" %}
</div>
</div>
</div>

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block title %}Зареждане на сметка{% endblock %}
{% block page_content %}
<div class="container-fluid">
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">Зареждане на сметка</div>
<div class="panel-body">
<form method="POST" action="{{ url_for('admin.charge', user_pid=usr.pid) }}">
<p>
Current Value: {{ usr.wallet }}<br />
{{ form.amount.label }} {{ form.amount }}<br />
{% for error in form.amount.errors %}
{{ error }}<br />
{% endfor %}
</p>
</div>
</div>
<p>
{{ form.csrf_token() }}
{{ form.submit }}
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="addresses">
<div class="panel-heading">Addresses</div>
<div class="panel-body"><p>
{% include "admin/menu_deployments.html" %}
<button class="btn btn-default" onclick="window.location.href='{{ url_for('admin.addr2pool') }}'"><span class="glyphicon glyphicon-plus" aria-hiddent="true"></span> Add IP to pool</button>
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<!--<th>Region</th>-->
<th>IP</th>
<th>Assignee</th>
<th>Server</th>
<th>VLAN</th>
<th>Deploy</th>
<th>rDNS</th>
</tr>
</thead>
<tbody>
{% for address in addresses %}
<tr>
{% if address.reserved == True %}<tr class="danger">{% else %}<tr>{% endif %}
<!--<td data-title="Region">region1</td>-->
<td data-title="IP">{{ address.ip }}</td>
{% if address.user_id != None %}
<td data-title="Assignee"><a href="{{ url_for('panel.dashboard', user_pid=address.user_id) }}">{{ address.owner.email }}</a></td>
{% else %}
<td></td>
{% endif %}
{% if address.assignee != None %}
<td data-title="Server">{{ address.assignee.deploy.server.name }}</td>
<td data-title="VLAN">{{ address.assignee.vlan_id }}</td>
<td data-title="Deploy">{{ address.assignee.deploy.machine_alias }}</td>
{% else %}
<td></td>
<td></td>
<td></td>
{% endif %}
{% if address.rdns != None %}
<td data-title="rDNS">{{ address.rdns }}</td>
{% else %}
<td></td>
{% endif %}
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,123 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
</style>
{% endblock %}
{% block scripts %}
{{ super() }}
<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();
if (window.confirm("Are you sure?")) {
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", "/vmanager/command/" + command + "/" + vmid, true);
// and then we send it off
request.send();
alert("command " + command + " executed.");
window.location.reload();
}
});
}
}, true);
</script>
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="deployments">
<div class="panel-heading">Deployments</div>
<div class="panel-body"><p>
{% include "admin/menu_deployments.html" %}
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>Owner</th>
<th>Alias</th>
<th>CPU</th>
<th>Mem</th>
<th>HDD</th>
<th>Last Charged</th>
<th>Days Left</th>
<th></th>
</tr>
</thead>
<tbody>
{% for deploy in deployments %}
{% if deploy.deleted == True %}
<tr class="active">
{% else %}
{% if deploy.enabled == False %}
<tr class="danger">
{% else %}
{% if deploy.warning == True %}
<tr class="warning">
{% else %}
<tr>
{% endif %}
{% endif %}
{% endif %}
<td><a href="{{ url_for('panel.dashboard', user_pid=deploy.user_id) }}">{{ deploy.owner.email }}</a></td>
<td>{{ deploy.machine_alias }}</font></td>
<td>{{ deploy.machine_cpu }}</td>
<td>{{ deploy.machine_mem }} MB</td>
<td>{{ deploy.machine_hdd }} GB</td>
{% if deploy.date_last_charge == None %}
<td>Never</td>
{% else %}
<td>{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }})</td>
{% endif %}
<td>{{ deploy.daysleft }}</td>
{% if deploy.deleted == True %}
<td>-deleted-</td>
{% else %}
<td><button class="btn btn-default btn-danger" onclick="location.reload();location.href='/vmanager/vmremove/{{ deploy.machine_id }}'"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</button></td>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
</style>
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="deployments">
<div class="panel-heading">Deployments</div>
<div class="panel-body"><p>
{% include "admin/menu_deployments.html" %}
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>Server</th>
<th>VLAN</th>
<th>Alias</th>
<th>CPU</th>
<th>Mem</th>
<th>HDD</th>
<th>Last Charged</th>
<th>Days Left</th>
<th>Owner</th>
</tr>
</thead>
<tbody>
{% for deploy in deployments %}
{% if deploy.enabled == False %}
<tr class="danger">
{% else %}
{% if deploy.warning == True %}
<tr class="warning">
{% else %}
<tr>
{% endif %}
{% endif %}
<td data-title="Server">{{ deploy.server.name }}</td>
<td data-title="VLAN">{% for vlan in deploy.inv_pubvlans %}{{ vlan.vlan_id }}{% endfor %}</td>
<td data-title="Alias"><a class="rrd" data-toggle="tooltip" title="Status: {{ status[deploy.machine_id] }} - Protected: {{ deploy.protected }} - ID: {{ deploy.machine_id }}"><b>{% if status[deploy.machine_id] == 'running' %}<font color="green">{% else %}{% if status[deploy.machine_id] == 'stopped' %}<font color="olive">{% else %}<font color="red">{% endif %}{% endif %}{{ deploy.machine_alias }}</font></b></a></td>
<td data-title="CPU">{{ deploy.machine_cpu }}</td>
<td data-title="Memory">{{ deploy.machine_mem }} MB</td>
<td data-title="HDD">{{ deploy.machine_hdd }} GB</td>
{% if deploy.date_last_charge == None %}
<td data-title="Last Charged">Never</td>
{% else %}
<td data-title="Last Charged">{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }})</td>
{% endif %}
<td data-title="Days Left">{{ deploy.daysleft }}</td>
<td data-title="Owner"><a href="{{ url_for('panel.dashboard', user_pid=deploy.user_id) }}">{{ deploy.owner.email }}</a></td>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
</style>
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="domains">
<div class="panel-heading">Domains</div>
<div class="panel-body"><p>
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>Owner</th>
<th>Name</th>
<th>Expiry Date</th>
<th>Days Left</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
{% if domain.enabled == False %}
<tr class="danger">
{% else %}
{% if domain.warning == True %}
<tr class="warning">
{% else %}
<tr>
{% endif %}
{% endif %}
<td><a href="{{ url_for('panel.dashboard', user_pid=domain.user_id) }}">{{ domain.owner.email }}</a></td>
<td><b><a href="http://{{ domain.fqdn }}">{{ domain.fqdn }}</a></b></td>
<td>{{ domain.date_expire }}</td>
<td>{{ domain.daysleft }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-danger" id="orders">
<div class="panel-heading">Orders</div>
<div class="panel-body"><p>
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<!--<th>Region</th>-->
<th>User</th>
<th>Region</th>
<th>Recipe</th>
<th>param 1</th>
<th>param 2</th>
<th>param 3</th>
<th>param 4</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for order in neworders %}
<tr>
<td data-title="User"><a href="{{ url_for('panel.dashboard', user_pid=order.user_id) }}">{{ order.owner.email }}</a></td>
<td data-title="Region">{{ order.region.description }}</td>
<td data-title="Recipe"><a href="#" title="{{ order.recipe.description }}">{{ order.recipe.templatefile }}</a></td>
<td data-title="Parameter1">{{ order.parameter1 }}</td>
<td data-title="Parameter2">{{ order.parameter2 }}</td>
<td data-title="Parameter3">{{ order.parameter3 }}</td>
<td data-title="Parameter4">{{ order.parameter4 }}</td>
<td data-title="Status">{{ order.status }}</td>
<td><button class="btn btn-default btn-success" onclick="window.open('{{ url_for('vmanager.vmcreate', orderid=order.pid) }}','_self');"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Confirm</button></td>
{% endfor %}
{% for order in oldorders %}
<tr>
<td data-title="User"><a href="{{ url_for('panel.dashboard', user_pid=order.user_id) }}">{{ order.owner.email }}</a></td>
<td data-title="Region">{{ order.region.description }}</td>
<td data-title="Recipe"><a href="#" title="{{ order.recipe.description }}">{{ order.recipe.templatefile }}</a></td>
<td data-title="Parameter1">{{ order.parameter1 }}</td>
<td data-title="Parameter2">{{ order.parameter2 }}</td>
<td data-title="Parameter3">{{ order.parameter3 }}</td>
<td data-title="Parameter4">{{ order.parameter4 }}</td>
<td data-title="Status">{{ order.status }}</td>
<td></td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">Servers</div>
<div class="panel-body">
{% include "admin/menu_deployments.html" %}
<div class="table-responsive">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>Name</th>
<th>CPU</th>
<th>MEM</th>
<th>HDD</th>
<th>Address</th>
<th>Region</th>
<th>Seller</th>
</tr>
</thead>
<tbody>
{% for server in servers %}
<tr class="default">
<td>{{ server.name }}</td>
<td>{{ server.cpu }}</td>
<td>{{ server.mem }}</td>
<td>{{ server.hdd }}</td>
<td>{{ server.address }}</td>
<td>{{ server.region.name }}</td>
<td><a href="{{ url_for('panel.dashboard', user_pid=server.owner.pid) }}">{{ server.owner.email }}</a></td>
</tr>
</tbody>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block styles %}
{{ super() }}
</style>
{% endblock %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="services">
<div class="panel-heading">Services</div>
<div class="panel-body"><p>
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>Owner</th>
<th>Category</th>
<th>Description</th>
<th>Price</th>
<th>Last Charged</th>
<th>Days Left</th>
</tr>
</thead>
<tbody>
{% for service in services %}
{% if service.enabled == False %}
<tr class="danger">
{% else %}
{% if service.warning == True %}
<tr class="warning">
{% else %}
<tr>
{% endif %}
{% endif %}
<td data-title="Owner"><a href="{{ url_for('panel.dashboard', user_pid=service.user_id) }}">{{ service.owner.email }}</a></td>
<td data-title="Category">{{ service.category }}</td>
<td data-title="Description">{{ service.description }}</td>
<td data-title="Price">{{ service.price }}</td>
<td data-title="Last Charged">{{ moment(service.date_last_charge).format('ll') }} ({{ moment(service.date_last_charge).fromNow() }})</td>
<td data-title="Days Left">{{ service.daysleft }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading">All Transactions</div>
<div class="panel-body">
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<th>ID</th>
<th>Description</th>
<th>Amount</th>
<th>Date</th>
<th>User</th>
</tr>
</thead>
<tbody>
{% for transaction in transactions %}
{% if transaction.value > 0 %}
<tr class="default">
<td data-title="ID">{{ transaction.pid }}</td>
<td data-title="Description">{{ transaction.description }}</td>
<td data-title="Amount">{{ transaction.value }} {{ transaction.currency }}</td>
<td data-title="Date">{{ moment(transaction.date_created).format('lll') }}</td>
<td data-title="User"><a href="{{ url_for('admin.transaction', user_pid=transaction.owner.pid) }}">{{ transaction.owner.email }}</a></td>
{% else %}
<tr class="default">
<td data-title="ID">{{ transaction.pid }}</td>
<td data-title="Description">{{ transaction.description }}</td>
<td data-title="Amount">{{ transaction.value }} {{ transaction.currency }}</td>
<td data-title="Date">{{ moment(transaction.date_created).format('lll') }}</td>
<td data-title="User"><a href="{{ url_for('admin.transaction', user_pid=transaction.owner.pid) }}">{{ transaction.owner.email }}</a></td>
{% endif %}
</tr>
</tbody>
{% endfor %}
</table>
{% if transactions.has_prev %}<a href="{{ url_for('view', page=transactions.prev_num) }}">&lt;&lt; Previous</a>{% else %}&lt;&lt; Previous{% endif %} |
{% if transactions.has_next %}<a href="{{ url_for('view', page=transactions.next_num) }}">Next &gt;&gt;</a>{% else %}Next &gt;&gt;{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block page_content %}
<div class="row">
{% include "admin/admin_tasks.html" %}
<div class="col-md-12">
<div class="panel panel-info" id="users">
<div class="panel-heading">List Active Users</div>
<div class="panel-body"><p>
<div class="no-more-tables">
<table class="table table-hover table-striped table-condensed cf">
<thead>
<tr>
<td>email</td>
<td>last seen</td>
<td>last ip</td>
<td>wallet</td>
<td>currency</td>
<td></td>
</tr>
</thead>
<tbody>
{% for usr in users %}
<tr>
<td data-title="Email"><font {% if usr.is_administrator() == True %}color="red"{% endif %}>{{ usr.email }}</td>
<td data-title="Last Seen">{{ moment(usr.last_seen).format('lll') }}</td>
<td data-title="Last IP"><a href="https://apps.db.ripe.net/search/query.html?searchtext={{ usr.last_ip }}" data-toggle="tooltip" title="RIPE Whois Search" target="_blank">{{ usr.last_ip }}</a></td>
<td data-title="Wallet">{{ usr.wallet }}</td>
<td data-title="Currency">{{ usr.currency }}</td>
<td><a href="{{ url_for('admin.charge', user_pid=usr.pid) }}" data-toggle="tooltip" title="Add Funds"><span class="glyphicon glyphicon-plus"></span></a>
<a href="{{ url_for('admin.transaction', user_pid=usr.pid) }}" data-toggle="tooltip" title="List Transactions"><span class="glyphicon glyphicon-credit-card"></span></a>
<a href="{{ url_for('panel.dashboard', user_pid=usr.pid) }}" data-toggle="tooltip" title="Show Dashboard"><span class="glyphicon glyphicon-modal-window"></span></a>
</tr>
{% endfor %}
</tbody>
</table>
{% if users.has_prev %}<a href="{{ url_for('view', page=users.prev_num) }}">&lt;&lt; Previous</a>{% else %}&lt;&lt; Previous{% endif %} |
{% if users.has_next %}<a href="{{ url_for('view', page=users.next_num) }}">Next &gt;&gt;</a>{% else %}Next &gt;&gt;{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
</div>
{% endblock %}

View file

@ -0,0 +1,6 @@
<button class="btn btn-danger btn-md" onclick="window.open('{{ url_for('admin.list_orders') }}','_self')"><span class="glyphicon glyphicon-bell" aria-hidden="true"></span> Orders</button>
<button class="btn btn-success btn-md" onclick="window.open('{{ url_for('admin.list_deployments') }}','_self')"><span class="glyphicon glyphicon-hdd" aria-hidden="true"></span> Deployments</button>
<button class="btn btn-success btn-md" onclick="window.open('{{ url_for('admin.list_services') }}','_self')"><span class="glyphicon glyphicon-star" aria-hidden="true"></span> Services</button>
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_users') }}','_self')"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Users</button>
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_transactions') }}','_self')"><span class="glyphicon glyphicon-btc" aria-hidden="true"></span> Transactions</button>

View file

@ -0,0 +1,4 @@
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_addresses') }}','_self')"><span class="glyphicon glyphicon-tags" aria-hidden="true"></span> Addresses</button>
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_servers') }}','_self')"><span class="glyphicon glyphicon-off" aria-hidden="true"></span> Servers</button>
<button class="btn btn-primary btn-md" onclick="window.open('{{ url_for('admin.list_archive') }}','_self')"><span class="glyphicon glyphicon-folder-close" aria-hidden="true"></span> Archive</button>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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