Merged in develop (pull request #44)

Version 0.4.0 release
master
Ensar Sarajcic 2018-11-01 00:47:03 +00:00
commit afd6d30a69
14 changed files with 1036 additions and 22 deletions

View File

@ -33,15 +33,15 @@ def confirm_email_token(token):
try: try:
email = confirm_token(token) email = confirm_token(token)
except Exception: except Exception:
return False return False, None
user = Account.query.filter_by(email=email).first_or_404() user = Account.query.filter_by(email=email).first_or_404()
if user.confirmed: if user.confirmed:
return True return True, user.email
else: else:
user.confirmed = True user.confirmed = True
user.confirmed_on = datetime.datetime.now() user.confirmed_on = datetime.datetime.now()
user.save() user.save()
return True return True, user.email
def update_account_role(account_id, role_id): def update_account_role(account_id, role_id):

View File

@ -23,6 +23,9 @@ class Account(db.Model):
default=db.func.current_timestamp(), default=db.func.current_timestamp(),
onupdate=db.func.current_timestamp()) onupdate=db.func.current_timestamp())
dashboards = db.relationship("Dashboard",
cascade="save-update, merge, delete")
def __init__(self, username, password, email, role=2): def __init__(self, username, password, email, role=2):
self.username = str(username) self.username = str(username)
self.password = str(password) self.password = str(password)

View File

@ -23,7 +23,9 @@ def add_resources():
DeviceTypeResource, DeviceTypeResource,
DeviceTypeListResource, DeviceTypeListResource,
DeviceConfigurationResource, DeviceConfigurationResource,
DeviceSecretResource) DeviceSecretResource,
DeviceShareResource,
DeviceShareActivationResource)
from .resources.dashboard import (DashboardResource, from .resources.dashboard import (DashboardResource,
DashboardListResource, DashboardListResource,
DashboardWidgetResource, DashboardWidgetResource,
@ -53,6 +55,11 @@ def add_resources():
'/v1/devices/<int:device_id>/configuration') '/v1/devices/<int:device_id>/configuration')
api.add_resource(DeviceSecretResource, api.add_resource(DeviceSecretResource,
'/v1/devices/<int:device_id>/secret') '/v1/devices/<int:device_id>/secret')
api.add_resource(DeviceShareResource,
'/v1/devices/<int:device_id>/share')
api.add_resource(
DeviceShareActivationResource,
'/v1/devices/<int:device_id>/share/activate/<string:token>')
api.add_resource(DashboardResource, api.add_resource(DashboardResource,
'/v1/dashboards/<int:dashboard_id>') '/v1/dashboards/<int:dashboard_id>')
api.add_resource(DashboardListResource, '/v1/dashboards') api.add_resource(DashboardListResource, '/v1/dashboards')

View File

@ -1,5 +1,5 @@
from flask_restful import Resource, abort from flask_restful import Resource, abort
from flask import g, render_template from flask import g, render_template, redirect
from marshmallow import Schema, fields from marshmallow import Schema, fields
from webargs.flaskparser import use_args from webargs.flaskparser import use_args
from flasgger import swag_from from flasgger import swag_from
@ -10,6 +10,7 @@ from app.api.auth_protection import ProtectedResource
from app.api.permission_protection import (requires_permission, from app.api.permission_protection import (requires_permission,
valid_permissions) valid_permissions)
from app.api.schemas import BaseResourceSchema from app.api.schemas import BaseResourceSchema
from flask import current_app as app
class UserSchema(BaseResourceSchema): class UserSchema(BaseResourceSchema):
@ -98,7 +99,7 @@ class AccountListResource(Resource):
confirm_url=confirm_url) confirm_url=confirm_url)
send_email_task.delay( send_email_task.delay(
args['email'], args['email'],
'Please confirm your email', 'ETF IoT Email confirmation',
html) html)
return UserSchema().dump(created_account), 201 return UserSchema().dump(created_account), 201
except ValueError: except ValueError:
@ -107,10 +108,17 @@ class AccountListResource(Resource):
class AccountEmailTokenResource(Resource): class AccountEmailTokenResource(Resource):
def get(self, token): def get(self, token):
success = accounts.confirm_email_token(token) success, email = accounts.confirm_email_token(token)
if success: if success:
return '{"status": "success", \ frontend_url = app.config['FRONTEND_URL']
"message": "Successfully confirmed email"}', 200 html = render_template(
'welcome_to_iot.html',
frontend_url=frontend_url)
send_email_task.delay(
email,
'Welcome to ETF IoT!',
html)
return redirect(frontend_url)
class AccountEmailTokenResendResource(Resource): class AccountEmailTokenResendResource(Resource):

View File

@ -2,10 +2,12 @@ from flask_restful import abort
from marshmallow import Schema, fields from marshmallow import Schema, fields
from webargs.flaskparser import use_args from webargs.flaskparser import use_args
from flasgger import swag_from from flasgger import swag_from
from flask import g, request from flask import g, request, redirect
from app.api.blueprint import api
import app.devices.api as devices import app.devices.api as devices
from app.api.auth_protection import ProtectedResource from app.api.auth_protection import ProtectedResource
from app.api.schemas import BaseResourceSchema from app.api.schemas import BaseResourceSchema
from flask import current_app as app
class BasicDeviceTypeSchema(Schema): class BasicDeviceTypeSchema(Schema):
@ -46,6 +48,16 @@ class DeviceSecretSchema(BaseResourceSchema):
secret_algorithm = fields.String() secret_algorithm = fields.String()
class DeviceShareSchema(BaseResourceSchema):
access_level_id = fields.Integer()
account_id = fields.Integer(required=False)
class DeviceShareTokenSchema(BaseResourceSchema):
token = fields.String()
activation_url = fields.String()
def validate_device_ownership(device_id): def validate_device_ownership(device_id):
if not devices.can_user_access_device(g.current_account.id, device_id): if not devices.can_user_access_device(g.current_account.id, device_id):
abort(403, message='You are not allowed to access this device', abort(403, message='You are not allowed to access this device',
@ -158,3 +170,36 @@ class DeviceSecretResource(ProtectedResource):
def get(self, device_id): def get(self, device_id):
validate_device_ownership(device_id) validate_device_ownership(device_id)
return DeviceSecretSchema().dump(devices.get_device(device_id)), 200 return DeviceSecretSchema().dump(devices.get_device(device_id)), 200
class DeviceShareResource(ProtectedResource):
@use_args(DeviceShareSchema(), locations=('json',))
@swag_from('swagger/create_device_share_token_spec.yaml')
def post(self, args, device_id):
validate_device_ownership(device_id)
created_token = devices.create_targeted_device_sharing_token(
device_id, args['access_level_id'], args.get('account_id'))
activation_url = api.url_for(
DeviceShareActivationResource,
device_id=device_id,
token=created_token, _external=True)
return DeviceShareTokenSchema().dump(
{
'token': created_token,
'activation_url': activation_url
}
), 201
class DeviceShareActivationResource(ProtectedResource):
def get(self, device_id, token):
try:
success = devices.activate_device_sharing_token(
g.current_account.id, token)
if not success:
abort(403,
message='You may not get access to this device',
status='error')
return redirect(app.config['FRONTEND_URL'])
except ValueError as e:
abort(400, message=str(e), status='error')

View File

@ -0,0 +1,21 @@
Creates new device sharing token
---
tags:
- Device
parameters:
- in: body
name: body
required: true
schema:
type: object
$ref: '#/definitions/DeviceShareTokenCreation'
responses:
201:
description: Success
schema:
type: object
required:
- content
properties:
content:
$ref: '#/definitions/DeviceShareToken'

View File

@ -122,8 +122,10 @@ class DashboardWidget(db.Model):
__tablename__ = 'dashboard_widgets' __tablename__ = 'dashboard_widgets'
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
dashboard_id = db.Column(db.Integer, db.ForeignKey('dashboards.id')) dashboard_id = db.Column(db.Integer, db.ForeignKey('dashboards.id'),
device_id = db.Column(db.Integer, db.ForeignKey('devices.id')) nullable=False)
device_id = db.Column(db.Integer, db.ForeignKey('devices.id'),
nullable=False)
height = db.Column(db.Integer, nullable=False) height = db.Column(db.Integer, nullable=False)
width = db.Column(db.Integer, nullable=False) width = db.Column(db.Integer, nullable=False)
x = db.Column(db.Integer, nullable=False) x = db.Column(db.Integer, nullable=False)

View File

@ -1,7 +1,12 @@
import sys import sys
import hmac import hmac
import urllib.parse import urllib.parse
from .models import Device, Recording, DeviceAssociation, DeviceType from .models import (Device,
Recording,
DeviceAssociation,
DeviceType,
AccessLevel)
from itsdangerous import URLSafeSerializer
from app.core import app from app.core import app
from app.jsonql import api as jsonql from app.jsonql import api as jsonql
@ -263,8 +268,69 @@ def create_recording(device_id, raw_json):
recording.save() recording.save()
def create_targeted_device_sharing_token(
device_id, access_level_id, account_id=None):
"""
Creates device sharing token that can be passed only to account with passed
id in order to allow access to device
:param device_id: Id of device
:type device_id: int
:param access_level_id: Id of access level this link will give
:type access_level_id: int
:param account_id: Id of account
:type account_id: int
:raises: ValueError if device does not exist
"""
if not Device.exists(id=device_id):
raise ValueError("Device does not exist!")
if not AccessLevel.exists(id=access_level_id):
raise ValueError("AccessLevel does not exist!")
data_to_serialize = {
'device_id': device_id,
'access_level_id': access_level_id
}
if account_id is not None:
data_to_serialize['account_id'] = account_id
serializer = URLSafeSerializer(app.config['SECRET_KEY'],
salt=app.config['SECURITY_PASSWORD_SALT'])
token = serializer.dumps(data_to_serialize)
return token
def activate_device_sharing_token(account_id, token):
"""
Activates device sharing token for account with passed id
:param account_id: Id of account
:type account_id: int
:param token: Token created by device owner
:type token: string
:raises: ValueError if device does not exist
"""
serializer = URLSafeSerializer(app.config['SECRET_KEY'],
salt=app.config['SECURITY_PASSWORD_SALT'])
token_data = serializer.loads(token)
device_id = token_data['device_id']
access_level_id = token_data['access_level_id']
if (token_data.get('account_id') or account_id) != account_id:
return False
if not Device.exists(id=device_id):
raise ValueError("Device does not exist!")
device_association = DeviceAssociation(device_id, account_id,
access_level_id)
device_association.save()
return True
def run_custom_query(device_id, request): def run_custom_query(device_id, request):
""" """
Runs custom query as defined by jsonql module
""" """
if not Device.exists(id=device_id): if not Device.exists(id=device_id):
raise ValueError("Device does not exist!") raise ValueError("Device does not exist!")

View File

@ -135,6 +135,8 @@ class Device(db.Model):
cascade="save-update, merge, delete") cascade="save-update, merge, delete")
recordings = db.relationship("Recording", recordings = db.relationship("Recording",
cascade="save-update, merge, delete") cascade="save-update, merge, delete")
widgets = db.relationship("DashboardWidget",
cascade="save-update, merge, delete")
def __init__(self, name, configuration=None, device_type=1): def __init__(self, name, configuration=None, device_type=1):
self.name = name self.name = name
@ -224,6 +226,9 @@ class DeviceAssociation(db.Model):
access_level = db.Column(db.Integer, db.ForeignKey('access_levels.id'), access_level = db.Column(db.Integer, db.ForeignKey('access_levels.id'),
nullable=False) nullable=False)
access_level_data = db.relationship("AccessLevel",
foreign_keys=[access_level])
def __init__(self, device_id, account_id, access_level=1): def __init__(self, device_id, account_id, access_level=1):
self.device_id = device_id self.device_id = device_id
self.account_id = account_id self.account_id = account_id
@ -327,9 +332,20 @@ class AccessLevel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String, nullable=False) name = db.Column(db.String, nullable=False)
permissions = db.Column(db.ARRAY(db.String), nullable=False)
def __init__(self, name): def __init__(self, name, permissions=['VIEW_DEVICE']):
self.name = name self.name = name
self.permissions = permissions
@staticmethod
def exists(**kwargs):
"""
Checks if access level with all of the given arguments exists
"""
if AccessLevel.query.filter_by(**kwargs).first():
return True
return False
def __repr__(self): def __repr__(self):
return '<AccessLevel (name %s)>' % self.name return '<AccessLevel (name %s)>' % self.name

View File

@ -227,6 +227,31 @@ definitions:
device_type: device_type:
$ref: '#/definitions/DeviceType' $ref: '#/definitions/DeviceType'
DeviceShareTokenCreation:
type: object
required:
- access_level_id
properties:
access_level_id:
$ref: '#/definitions/id'
account_id:
$ref: '#/definitions/id'
DeviceShareToken:
type: object
required:
- token
- activation_url
properties:
token:
type: string
description: Activation token used to gain access to shared device
example: idjsfodsfmskdf12312nkVDSFSDFS
activation_url:
type: string
description: Activation url using token
example: https://etf-iot.com/api/v1/devices/123/share/activation/idjsfodsfmskdf12312nkVDSFSDFS
DeviceWithConfig: DeviceWithConfig:
type: object type: object
required: required:

View File

@ -1,4 +1,387 @@
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p> <!doctype html>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p> <html>
<br> <head>
<p>Cheers!</p> <meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Please activate your email for ETF IoT</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">Welcome to ETF IoT! Please confirm your
email.</span>
<table role="presentation" class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<img
src="http://www.etf.unsa.ba/etf/css/images/etf-dugi.gif"/>
<p>Welcome to ETF IoT!</p>
<p>This email was used for registration of an account
on ETF IoT system. <i>If you did not do that, you can
safely ignore this email.</i>
Before you get started, confirm your email.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{confirm_url}}"
target="_blank">Confirm email</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p>If you can't see the button, click on the following
link or copy it in your browser: <a
href="{{confirm_url}}">{{confirm_url}}</a></p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,382 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Welcome to IoT!</title>
<style>
/* -------------------------------------
GLOBAL RESETS
------------------------------------- */
/*All the styling goes here*/
img {
border: none;
-ms-interpolation-mode: bicubic;
max-width: 100%;
}
body {
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
table {
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%; }
table td {
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
}
/* -------------------------------------
BODY & CONTAINER
------------------------------------- */
.body {
background-color: #f6f6f6;
width: 100%;
}
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
.container {
display: block;
Margin: 0 auto !important;
/* makes it centered */
max-width: 580px;
padding: 10px;
width: 580px;
}
/* This should also be a block element, so that it will fill 100% of the .container */
.content {
box-sizing: border-box;
display: block;
Margin: 0 auto;
max-width: 580px;
padding: 10px;
}
/* -------------------------------------
HEADER, FOOTER, MAIN
------------------------------------- */
.main {
background: #ffffff;
border-radius: 3px;
width: 100%;
}
.wrapper {
box-sizing: border-box;
padding: 20px;
}
.content-block {
padding-bottom: 10px;
padding-top: 10px;
}
.footer {
clear: both;
Margin-top: 10px;
text-align: center;
width: 100%;
}
.footer td,
.footer p,
.footer span,
.footer a {
color: #999999;
font-size: 12px;
text-align: center;
}
/* -------------------------------------
TYPOGRAPHY
------------------------------------- */
h1,
h2,
h3,
h4 {
color: #000000;
font-family: sans-serif;
font-weight: 400;
line-height: 1.4;
margin: 0;
margin-bottom: 30px;
}
h1 {
font-size: 35px;
font-weight: 300;
text-align: center;
text-transform: capitalize;
}
p,
ul,
ol {
font-family: sans-serif;
font-size: 14px;
font-weight: normal;
margin: 0;
margin-bottom: 15px;
}
p li,
ul li,
ol li {
list-style-position: inside;
margin-left: 5px;
}
a {
color: #3498db;
text-decoration: underline;
}
/* -------------------------------------
BUTTONS
------------------------------------- */
.btn {
box-sizing: border-box;
width: 100%; }
.btn > tbody > tr > td {
padding-bottom: 15px; }
.btn table {
width: auto;
}
.btn table td {
background-color: #ffffff;
border-radius: 5px;
text-align: center;
}
.btn a {
background-color: #ffffff;
border: solid 1px #3498db;
border-radius: 5px;
box-sizing: border-box;
color: #3498db;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-weight: bold;
margin: 0;
padding: 12px 25px;
text-decoration: none;
text-transform: capitalize;
}
.btn-primary table td {
background-color: #3498db;
}
.btn-primary a {
background-color: #3498db;
border-color: #3498db;
color: #ffffff;
}
/* -------------------------------------
OTHER STYLES THAT MIGHT BE USEFUL
------------------------------------- */
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.clear {
clear: both;
}
.mt0 {
margin-top: 0;
}
.mb0 {
margin-bottom: 0;
}
.preheader {
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
}
.powered-by a {
text-decoration: none;
}
hr {
border: 0;
border-bottom: 1px solid #f6f6f6;
Margin: 20px 0;
}
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
<tr>
<td>&nbsp;</td>
<td class="container">
<div class="content">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader">Welcome to ETF IoT! Please confirm your
email.</span>
<table role="presentation" class="main">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<img
src="http://www.etf.unsa.ba/etf/css/images/etf-dugi.gif"/>
<p>Welcome to ETF IoT!</p>
<p>You have successfully confirmed your email. You can
now use the system.</p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
<td align="left">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td> <a href="{{frontend_url}}"
target="_blank">Go to ETF IoT</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -2,7 +2,7 @@ import os
# App configuration # App configuration
DEBUG = os.environ['DEBUG'] DEBUG = os.environ['DEBUG']
APP_VERSION = '0.3.6' APP_VERSION = '0.4.0'
# Define the application directory # Define the application directory
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) BASE_DIR = os.path.abspath(os.path.dirname(__file__))
@ -23,11 +23,13 @@ CSRF_ENABLED = True
# Use a secure, unique and absolutely secret key for # Use a secure, unique and absolutely secret key for
# signing the data. # signing the data.
CSRF_SESSION_KEY = "secret" CSRF_SESSION_KEY = os.environ.get('CSRF_SECRET') or "secret"
# Secret key for signing cookies # Secret key for signing cookies
SECRET_KEY = "?['Z(Z\x83Y \x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld" SECRET_KEY = (os.environ.get('APP_SECRET_KEY') or
SECURITY_PASSWORD_SALT = "IyoZvOJb4feT3xKlYXyOJveHSIY4GDg6" "?['Z(Z\x83Y\x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld")
SECURITY_PASSWORD_SALT = (os.environ.get('APP_SECRETS_SALT') or
"IyoZvOJb4feT3xKlYXyOJveHSIY4GDg6")
# MQTT configuration # MQTT configuration
MQTT_CLIENT_ID = 'final-iot-backend-server-' + os.environ['MQTT_CLIENT'] MQTT_CLIENT_ID = 'final-iot-backend-server-' + os.environ['MQTT_CLIENT']
@ -55,6 +57,10 @@ MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
# mail accounts # mail accounts
MAIL_DEFAULT_SENDER = 'final.iot.backend.mailer@gmail.com' MAIL_DEFAULT_SENDER = 'final.iot.backend.mailer@gmail.com'
# frontend
FRONTEND_URL = (os.environ.get('IOT_FRONTEND_URL') or
'http://iot-frontend-app.herokuapp.com/')
# Flasgger config # Flasgger config
SWAGGER = { SWAGGER = {
'uiversion': 3 'uiversion': 3

View File

@ -0,0 +1,50 @@
"""empty message
Revision ID: 43e5ad1c4393
Revises: c09252c2c547
Create Date: 2018-11-01 00:58:07.570743
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '43e5ad1c4393'
down_revision = 'c09252c2c547'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('access_levels', sa.Column('permissions',
sa.ARRAY(sa.String()), nullable=True))
access_levels = sa.table('access_levels', sa.column('id', sa.Integer),
sa.column('permissions', sa.ARRAY(sa.String)))
op.execute(access_levels.update().where(access_levels.c.id == op.inline_literal(1)).
values({'permissions':
['VIEW_DEVICE', 'MODIFY_DEVICE',
'DELETE_DEVICE', 'WIDGET_READ',
'WIDGET_WRITE', 'CONFIGURATION_READ',
'CONFIGURATION_WRITE', 'SECRET_READ']})
)
op.alter_column('access_levels', 'permissions',
existing_type=sa.ARRAY(sa.String()),
nullable=False)
op.alter_column('dashboard_widgets', 'device_id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('dashboard_widgets', 'device_id',
existing_type=sa.INTEGER(),
nullable=True)
op.drop_column('access_levels', 'permissions')
# ### end Alembic commands ###