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:
email = confirm_token(token)
except Exception:
return False
return False, None
user = Account.query.filter_by(email=email).first_or_404()
if user.confirmed:
return True
return True, user.email
else:
user.confirmed = True
user.confirmed_on = datetime.datetime.now()
user.save()
return True
return True, user.email
def update_account_role(account_id, role_id):

View File

@ -23,6 +23,9 @@ class Account(db.Model):
default=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):
self.username = str(username)
self.password = str(password)

View File

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

View File

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

View File

@ -2,10 +2,12 @@ from flask_restful import abort
from marshmallow import Schema, fields
from webargs.flaskparser import use_args
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
from app.api.auth_protection import ProtectedResource
from app.api.schemas import BaseResourceSchema
from flask import current_app as app
class BasicDeviceTypeSchema(Schema):
@ -46,6 +48,16 @@ class DeviceSecretSchema(BaseResourceSchema):
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):
if not devices.can_user_access_device(g.current_account.id, device_id):
abort(403, message='You are not allowed to access this device',
@ -158,3 +170,36 @@ class DeviceSecretResource(ProtectedResource):
def get(self, device_id):
validate_device_ownership(device_id)
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'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
dashboard_id = db.Column(db.Integer, db.ForeignKey('dashboards.id'))
device_id = db.Column(db.Integer, db.ForeignKey('devices.id'))
dashboard_id = db.Column(db.Integer, db.ForeignKey('dashboards.id'),
nullable=False)
device_id = db.Column(db.Integer, db.ForeignKey('devices.id'),
nullable=False)
height = db.Column(db.Integer, nullable=False)
width = db.Column(db.Integer, nullable=False)
x = db.Column(db.Integer, nullable=False)

View File

@ -1,7 +1,12 @@
import sys
import hmac
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.jsonql import api as jsonql
@ -263,8 +268,69 @@ def create_recording(device_id, raw_json):
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):
"""
Runs custom query as defined by jsonql module
"""
if not Device.exists(id=device_id):
raise ValueError("Device does not exist!")

View File

@ -135,6 +135,8 @@ class Device(db.Model):
cascade="save-update, merge, delete")
recordings = db.relationship("Recording",
cascade="save-update, merge, delete")
widgets = db.relationship("DashboardWidget",
cascade="save-update, merge, delete")
def __init__(self, name, configuration=None, device_type=1):
self.name = name
@ -224,6 +226,9 @@ class DeviceAssociation(db.Model):
access_level = db.Column(db.Integer, db.ForeignKey('access_levels.id'),
nullable=False)
access_level_data = db.relationship("AccessLevel",
foreign_keys=[access_level])
def __init__(self, device_id, account_id, access_level=1):
self.device_id = device_id
self.account_id = account_id
@ -327,9 +332,20 @@ class AccessLevel(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
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.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):
return '<AccessLevel (name %s)>' % self.name

View File

@ -227,6 +227,31 @@ definitions:
device_type:
$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:
type: object
required:

View File

@ -1,4 +1,387 @@
<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
<br>
<p>Cheers!</p>
<!doctype html>
<html>
<head>
<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
DEBUG = os.environ['DEBUG']
APP_VERSION = '0.3.6'
APP_VERSION = '0.4.0'
# Define the application directory
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
# signing the data.
CSRF_SESSION_KEY = "secret"
CSRF_SESSION_KEY = os.environ.get('CSRF_SECRET') or "secret"
# Secret key for signing cookies
SECRET_KEY = "?['Z(Z\x83Y \x06T\x12\x96<\xff\x12\xe0\x1b\xd1J\xe0\xd9ld"
SECURITY_PASSWORD_SALT = "IyoZvOJb4feT3xKlYXyOJveHSIY4GDg6"
SECRET_KEY = (os.environ.get('APP_SECRET_KEY') or
"?['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_CLIENT_ID = 'final-iot-backend-server-' + os.environ['MQTT_CLIENT']
@ -55,6 +57,10 @@ MAIL_PASSWORD = os.environ['APP_MAIL_PASSWORD']
# mail accounts
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
SWAGGER = {
'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 ###