From c0f579ef2c2df03b01c78978d1a0e52e590a5455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 14:53:26 +0100 Subject: [PATCH 01/10] Fix datetime fields in selection for recordings --- .python-version | 1 + app/devices/api.py | 10 ++++++++-- app/jsonql/api.py | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..4f2c1d1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.6.6 diff --git a/app/devices/api.py b/app/devices/api.py index 514de4b..cee336d 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -7,7 +7,7 @@ from .models import (Device, DeviceType, AccessLevel) from itsdangerous import URLSafeSerializer -from app.core import app +from app.core import app, db from app.jsonql import api as jsonql @@ -335,7 +335,7 @@ def run_custom_query(device_id, request): if not Device.exists(id=device_id): raise ValueError("Device does not exist!") - def recording_field_provider(name): + def recording_field_provider(name, formatted=False): if name == 'record_value': return Recording.record_value if name == 'record_type': @@ -343,8 +343,14 @@ def run_custom_query(device_id, request): if name == 'device_id': return Recording.device_id if name == 'recorded_at': + if formatted: + return db.func.to_char( + Recording.recorded_at, 'YYYY-MM-DD"T"HH24:MI:SSOF') return Recording.recorded_at if name == 'received_at': + if formatted: + return db.func.to_char( + Recording.received_at, 'YYYY-MM-DD"T"HH24:MI:SSOF') return Recording.received_at resulting_query = jsonql.run_query_on(Recording.query.with_entities(), diff --git a/app/jsonql/api.py b/app/jsonql/api.py index e051343..0219e41 100644 --- a/app/jsonql/api.py +++ b/app/jsonql/api.py @@ -7,11 +7,20 @@ ORDERS = ['asc', 'desc'] def run_query_on(query_object, field_provider, **kwargs): + """ + Generates a query for target object based on query provided as kwargs + + :param query_object: Initial query object as returned by SQLAlchemy for + target table + :type query_object: Query + :param field_provider: Function which provides fields based on name, with + optional parameter formatted which returns the field formatted using sql + functions + :type field_provider: func(col_name:String, formatted:Boolean) + """ selections, filters, groups, orderings = validate_selections(**kwargs) entities = [] - print('Starting with args: ' + str(kwargs)) - if selections is not None: if groups is not None: for group in groups.keys(): @@ -26,9 +35,8 @@ def run_query_on(query_object, field_provider, **kwargs): for selection in selections.keys(): entities.append(get_column(selections[selection], - field_provider(selection)).label(selection)) + field_provider(selection, True)).label(selection)) - print('New entities: ' + str(entities)) query_object = query_object.with_entities(*entities) if filters is not None: From 584697e58e95f57bfd1ab8939138b01e1ee2c5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 15:03:07 +0100 Subject: [PATCH 02/10] Add updated and created at fields for tables --- app/accounts/models.py | 7 ++++ app/devices/models.py | 21 ++++++++++++ migrations/versions/764de3c39771_.py | 51 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 migrations/versions/764de3c39771_.py diff --git a/app/accounts/models.py b/app/accounts/models.py index d205226..102b02d 100644 --- a/app/accounts/models.py +++ b/app/accounts/models.py @@ -129,6 +129,13 @@ class Role(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) display_name = db.Column(db.String, unique=True) permissions = db.Column(db.ARRAY(db.String)) + created_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp()) + modified_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp()) def __init__(self, name, permissions): self.display_name = str(name) diff --git a/app/devices/models.py b/app/devices/models.py index e21d9ed..dac9c1a 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -225,6 +225,13 @@ class DeviceAssociation(db.Model): primary_key=True) access_level = db.Column(db.Integer, db.ForeignKey('access_levels.id'), nullable=False) + created_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp()) + modified_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp()) access_level_data = db.relationship("AccessLevel", foreign_keys=[access_level]) @@ -279,6 +286,13 @@ class DeviceType(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String, nullable=False) + created_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp()) + modified_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp()) def __init__(self, name): self.name = name @@ -333,6 +347,13 @@ 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) + created_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp()) + modified_at = db.Column(db.DateTime, + nullable=False, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp()) def __init__(self, name, permissions=['VIEW_DEVICE']): self.name = name diff --git a/migrations/versions/764de3c39771_.py b/migrations/versions/764de3c39771_.py new file mode 100644 index 0000000..52e5859 --- /dev/null +++ b/migrations/versions/764de3c39771_.py @@ -0,0 +1,51 @@ +"""empty message + +Revision ID: 764de3c39771 +Revises: 43e5ad1c4393 +Create Date: 2018-11-03 15:00:36.463124 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '764de3c39771' +down_revision = '43e5ad1c4393' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('access_levels', sa.Column('created_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('access_levels', sa.Column('modified_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('device_associations', sa.Column('created_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('device_associations', sa.Column('modified_at', + sa.DateTime(), nullable=False, + server_default=sa.func.current_timestamp())) + op.add_column('device_types', sa.Column('created_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('device_types', sa.Column('modified_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('roles', sa.Column('created_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + op.add_column('roles', sa.Column('modified_at', sa.DateTime(), + nullable=False, server_default=sa.func.current_timestamp())) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('roles', 'modified_at') + op.drop_column('roles', 'created_at') + op.drop_column('device_types', 'modified_at') + op.drop_column('device_types', 'created_at') + op.drop_column('device_associations', 'modified_at') + op.drop_column('device_associations', 'created_at') + op.drop_column('access_levels', 'modified_at') + op.drop_column('access_levels', 'created_at') + # ### end Alembic commands ### From fc00f96f0497a1e13294529d38fcc109216f1675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 15:22:35 +0100 Subject: [PATCH 03/10] Add create_at and modified_at on API for devices --- app/api/resources/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/resources/device.py b/app/api/resources/device.py index 664431e..2f5c2db 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -24,6 +24,8 @@ class DeviceSchema(BaseResourceSchema): name = fields.Str(required=True) device_type = fields.Nested(BasicDeviceTypeSchema, dump_only=True) device_type_id = fields.Integer(load_only=True, missing=1) + created_at = fields.DateTime(dump_only=True) + modified_at = fields.DateTime(dump_only=True) class DeviceWithConfigurationSchema(DeviceSchema): From 5d9763b386f8c67b2706944b9b291c22d009483f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 15:32:46 +0100 Subject: [PATCH 04/10] Use standard formatting for jsonql datetime columns --- app/devices/api.py | 13 +++++-------- app/jsonql/api.py | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/devices/api.py b/app/devices/api.py index cee336d..260e2d7 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -1,13 +1,14 @@ import sys import hmac import urllib.parse +import datetime from .models import (Device, Recording, DeviceAssociation, DeviceType, AccessLevel) from itsdangerous import URLSafeSerializer -from app.core import app, db +from app.core import app from app.jsonql import api as jsonql @@ -335,7 +336,7 @@ def run_custom_query(device_id, request): if not Device.exists(id=device_id): raise ValueError("Device does not exist!") - def recording_field_provider(name, formatted=False): + def recording_field_provider(name): if name == 'record_value': return Recording.record_value if name == 'record_type': @@ -343,14 +344,8 @@ def run_custom_query(device_id, request): if name == 'device_id': return Recording.device_id if name == 'recorded_at': - if formatted: - return db.func.to_char( - Recording.recorded_at, 'YYYY-MM-DD"T"HH24:MI:SSOF') return Recording.recorded_at if name == 'received_at': - if formatted: - return db.func.to_char( - Recording.received_at, 'YYYY-MM-DD"T"HH24:MI:SSOF') return Recording.received_at resulting_query = jsonql.run_query_on(Recording.query.with_entities(), @@ -363,6 +358,8 @@ def run_custom_query(device_id, request): for row in result: formatted_row = {} for idx, col in enumerate(row): + if isinstance(col, datetime.datetime): + col = col.replace(tzinfo=datetime.timezone.utc).isoformat() formatted_row[resulting_columns[idx]['name']] = col formatted_result.append(formatted_row) return formatted_result diff --git a/app/jsonql/api.py b/app/jsonql/api.py index 0219e41..8cf31e3 100644 --- a/app/jsonql/api.py +++ b/app/jsonql/api.py @@ -35,7 +35,7 @@ def run_query_on(query_object, field_provider, **kwargs): for selection in selections.keys(): entities.append(get_column(selections[selection], - field_provider(selection, True)).label(selection)) + field_provider(selection)).label(selection)) query_object = query_object.with_entities(*entities) From fa230f15e28774b85d23473a243141620f86b41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 15:47:38 +0100 Subject: [PATCH 05/10] Add name for widgets --- app/api/resources/dashboard.py | 22 ++++++++++++--------- app/dashboards/api.py | 10 +++++++--- app/dashboards/models.py | 4 +++- app/swagger/template.yaml | 9 +++++++++ migrations/versions/3cf41808886b_.py | 29 ++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 migrations/versions/3cf41808886b_.py diff --git a/app/api/resources/dashboard.py b/app/api/resources/dashboard.py index 9c4ada8..96fd211 100644 --- a/app/api/resources/dashboard.py +++ b/app/api/resources/dashboard.py @@ -11,13 +11,14 @@ from app.api.schemas import BaseResourceSchema class BasicDashboardWidgetSchema(Schema): id = fields.Integer(dump_only=True) - device_id = fields.Integer() - height = fields.Integer() - width = fields.Integer() - x = fields.Integer() - y = fields.Integer() - chart_type = fields.String() - filters = fields.Raw() + device_id = fields.Integer(required=True) + name = fields.String(required=True) + height = fields.Integer(required=True) + width = fields.Integer(required=True) + x = fields.Integer(required=True) + y = fields.Integer(required=True) + chart_type = fields.String(required=True) + filters = fields.Raw(required=True) class DashboardWidgetSchema(BaseResourceSchema, BasicDashboardWidgetSchema): @@ -27,8 +28,8 @@ class DashboardWidgetSchema(BaseResourceSchema, BasicDashboardWidgetSchema): class DashboardSchema(BaseResourceSchema): id = fields.Integer(dump_only=True) active = fields.Boolean(required=False) - dashboard_data = fields.Raw() - name = fields.String() + dashboard_data = fields.Raw(required=True) + name = fields.String(required=True) widgets = fields.Nested(BasicDashboardWidgetSchema, dump_only=True, many=True) @@ -112,6 +113,7 @@ class DashboardWidgetListResource(ProtectedResource): created_widget = dashboard.create_widget( dashboard_id, args['device_id'], + args['name'], args['height'], args['width'], args['x'], @@ -142,6 +144,7 @@ class DashboardWidgetResource(ProtectedResource): updated_widget = dashboard.patch_widget( widget_id, args['device_id'], + args['name'], args['height'], args['width'], args['x'], @@ -159,6 +162,7 @@ class DashboardWidgetResource(ProtectedResource): updated_widget = dashboard.patch_widget( widget_id, args.get('device_id'), + args.get('name'), args.get('height'), args.get('width'), args.get('x'), diff --git a/app/dashboards/api.py b/app/dashboards/api.py index 0e4a819..21af487 100644 --- a/app/dashboards/api.py +++ b/app/dashboards/api.py @@ -106,12 +106,13 @@ def get_dashboards(account_id, active): return Dashboard.get_many_filtered(account_id=account_id, active=active) -def create_widget(dashboard_id, device_id, height, width, x, y, +def create_widget(dashboard_id, device_id, name, height, width, x, y, chart_type, filters): """ Tries to create a dashboard widget """ - widget = DashboardWidget(dashboard_id, device_id, height, width, x, y, + widget = DashboardWidget(dashboard_id, device_id, + name, height, width, x, y, chart_type, filters) widget.save() return widget @@ -152,7 +153,7 @@ def get_widget(widget_id): return DashboardWidget.get(id=widget_id) -def patch_widget(widget_id, device_id=None, height=None, width=None, +def patch_widget(widget_id, device_id=None, name=None, height=None, width=None, x=None, y=None, chart_type=None, filters=None): """ Tries to update widget with given parameters @@ -165,6 +166,9 @@ def patch_widget(widget_id, device_id=None, height=None, width=None, if height is not None: widget.height = height + if name is not None: + widget.name = name + if width is not None: widget.width = width diff --git a/app/dashboards/models.py b/app/dashboards/models.py index 091052f..5e6aa61 100644 --- a/app/dashboards/models.py +++ b/app/dashboards/models.py @@ -126,6 +126,7 @@ class DashboardWidget(db.Model): nullable=False) device_id = db.Column(db.Integer, db.ForeignKey('devices.id'), nullable=False) + name = db.Column(db.String, nullable=False) height = db.Column(db.Integer, nullable=False) width = db.Column(db.Integer, nullable=False) x = db.Column(db.Integer, nullable=False) @@ -142,10 +143,11 @@ class DashboardWidget(db.Model): dashboard = db.relationship("Dashboard", foreign_keys=[dashboard_id]) - def __init__(self, dashboard_id, device_id, height, width, x, y, + def __init__(self, dashboard_id, device_id, name, height, width, x, y, chart_type, filters): self.dashboard_id = dashboard_id self.device_id = device_id + self.name = name self.height = height self.width = width self.x = x diff --git a/app/swagger/template.yaml b/app/swagger/template.yaml index f45f263..5d98951 100644 --- a/app/swagger/template.yaml +++ b/app/swagger/template.yaml @@ -219,6 +219,8 @@ definitions: - id - name - device_type + - created_at + - modified_at properties: id: $ref: '#/definitions/id' @@ -226,6 +228,10 @@ definitions: $ref: '#/definitions/devicename' device_type: $ref: '#/definitions/DeviceType' + created_at: + $ref: '#/definitions/datetime' + modified_at: + $ref: '#/definitions/datetime' DeviceShareTokenCreation: type: object @@ -328,6 +334,7 @@ definitions: - id - dashboard_id - device_id + - name - width - height - x @@ -341,6 +348,8 @@ definitions: $ref: '#/definitions/id' device_id: $ref: '#/definitions/id' + name: + $ref: '#/definitions/genericname' width: $ref: '#/definitions/id' height: diff --git a/migrations/versions/3cf41808886b_.py b/migrations/versions/3cf41808886b_.py new file mode 100644 index 0000000..80f338e --- /dev/null +++ b/migrations/versions/3cf41808886b_.py @@ -0,0 +1,29 @@ +"""empty message + +Revision ID: 3cf41808886b +Revises: 764de3c39771 +Create Date: 2018-11-03 15:40:04.384489 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3cf41808886b' +down_revision = '764de3c39771' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('dashboard_widgets', sa.Column('name', sa.String(), + nullable=False, server_default='Legacy widget')) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('dashboard_widgets', 'name') + # ### end Alembic commands ### From 79cbd09f24d1397fb7ae9a6e8d5bfd1bd3c3836e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 15:54:13 +0100 Subject: [PATCH 06/10] Add created_at and modified_at timestamps to all responses --- app/api/resources/account.py | 6 +++--- app/api/resources/dashboard.py | 8 +++++--- app/api/resources/device.py | 10 +++++----- app/api/schemas.py | 11 ++++++++++- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/app/api/resources/account.py b/app/api/resources/account.py index b36a49a..5b4e66e 100644 --- a/app/api/resources/account.py +++ b/app/api/resources/account.py @@ -9,11 +9,11 @@ from app.accounts.tasks import send_email_task from app.api.auth_protection import ProtectedResource from app.api.permission_protection import (requires_permission, valid_permissions) -from app.api.schemas import BaseResourceSchema +from app.api.schemas import BaseTimestampedResourceSchema from flask import current_app as app -class UserSchema(BaseResourceSchema): +class UserSchema(BaseTimestampedResourceSchema): username = fields.Str(required=True) email = fields.Email(required=True) password = fields.Str(required=True, load_only=True) @@ -27,7 +27,7 @@ def validate_role_permissions(permissions_list): return set(permissions_list).issubset(valid_permissions) -class RoleSchema(BaseResourceSchema): +class RoleSchema(BaseTimestampedResourceSchema): id = fields.Integer(required=True, location='json') display_name = fields.String(required=True, location='json') permissions = fields.List(fields.String, required=True, diff --git a/app/api/resources/dashboard.py b/app/api/resources/dashboard.py index 96fd211..2b80f88 100644 --- a/app/api/resources/dashboard.py +++ b/app/api/resources/dashboard.py @@ -6,10 +6,12 @@ from flasgger import swag_from import app.dashboards.api as dashboard import app.devices.api as device from app.api.auth_protection import ProtectedResource -from app.api.schemas import BaseResourceSchema +from app.api.schemas import (BaseResourceSchema, + BaseTimestampedSchema, + BaseTimestampedResourceSchema) -class BasicDashboardWidgetSchema(Schema): +class BasicDashboardWidgetSchema(BaseTimestampedSchema): id = fields.Integer(dump_only=True) device_id = fields.Integer(required=True) name = fields.String(required=True) @@ -25,7 +27,7 @@ class DashboardWidgetSchema(BaseResourceSchema, BasicDashboardWidgetSchema): pass -class DashboardSchema(BaseResourceSchema): +class DashboardSchema(BaseTimestampedResourceSchema): id = fields.Integer(dump_only=True) active = fields.Boolean(required=False) dashboard_data = fields.Raw(required=True) diff --git a/app/api/resources/device.py b/app/api/resources/device.py index 2f5c2db..ab9f627 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -6,11 +6,13 @@ 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 app.api.schemas import (BaseResourceSchema, + BaseTimestampedSchema, + BaseTimestampedResourceSchema) from flask import current_app as app -class BasicDeviceTypeSchema(Schema): +class BasicDeviceTypeSchema(BaseTimestampedSchema): id = fields.Integer(dump_only=True) name = fields.Str(required=True) @@ -19,13 +21,11 @@ class DeviceTypeSchema(BaseResourceSchema, BasicDeviceTypeSchema): pass -class DeviceSchema(BaseResourceSchema): +class DeviceSchema(BaseTimestampedResourceSchema): id = fields.Integer(dump_only=True) name = fields.Str(required=True) device_type = fields.Nested(BasicDeviceTypeSchema, dump_only=True) device_type_id = fields.Integer(load_only=True, missing=1) - created_at = fields.DateTime(dump_only=True) - modified_at = fields.DateTime(dump_only=True) class DeviceWithConfigurationSchema(DeviceSchema): diff --git a/app/api/schemas.py b/app/api/schemas.py index 8f4a228..2d12e43 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -1,7 +1,16 @@ -from marshmallow import Schema, post_dump +from marshmallow import Schema, post_dump, fields class BaseResourceSchema(Schema): @post_dump(pass_many=True) def wrap_with_envelope(self, data, many): return {'content': data} + + +class BaseTimestampedSchema(Schema): + created_at = fields.DateTime(dump_only=True) + modified_at = fields.DateTime(dump_only=True) + + +class BaseTimestampedResourceSchema(BaseResourceSchema, BaseTimestampedSchema): + pass From 06700e76aa6f73fc3f38afa01b539d910a0a7dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 16:00:46 +0100 Subject: [PATCH 07/10] Add route for resetting device secret --- app/api/blueprint.py | 3 +++ app/api/resources/device.py | 8 ++++++++ .../swagger/reset_device_secret_spec.yaml | 20 +++++++++++++++++++ app/devices/api.py | 12 +++++++++++ 4 files changed, 43 insertions(+) create mode 100644 app/api/resources/swagger/reset_device_secret_spec.yaml diff --git a/app/api/blueprint.py b/app/api/blueprint.py index 5dc5918..25a543c 100644 --- a/app/api/blueprint.py +++ b/app/api/blueprint.py @@ -24,6 +24,7 @@ def add_resources(): DeviceTypeListResource, DeviceConfigurationResource, DeviceSecretResource, + DeviceSecretResetResource, DeviceShareResource, DeviceShareActivationResource) from .resources.dashboard import (DashboardResource, @@ -55,6 +56,8 @@ def add_resources(): '/v1/devices//configuration') api.add_resource(DeviceSecretResource, '/v1/devices//secret') + api.add_resource(DeviceSecretResetResource, + '/v1/devices//secret/reset') api.add_resource(DeviceShareResource, '/v1/devices//share') api.add_resource( diff --git a/app/api/resources/device.py b/app/api/resources/device.py index ab9f627..a53b133 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -174,6 +174,14 @@ class DeviceSecretResource(ProtectedResource): return DeviceSecretSchema().dump(devices.get_device(device_id)), 200 +class DeviceSecretResetResource(ProtectedResource): + @swag_from('swagger/reset_device_secret_spec.yaml') + def post(self, device_id): + validate_device_ownership(device_id) + return DeviceSecretSchema().dump( + devices.reset_device_secret(device_id)), 200 + + class DeviceShareResource(ProtectedResource): @use_args(DeviceShareSchema(), locations=('json',)) @swag_from('swagger/create_device_share_token_spec.yaml') diff --git a/app/api/resources/swagger/reset_device_secret_spec.yaml b/app/api/resources/swagger/reset_device_secret_spec.yaml new file mode 100644 index 0000000..3219ac0 --- /dev/null +++ b/app/api/resources/swagger/reset_device_secret_spec.yaml @@ -0,0 +1,20 @@ +Resets a device secret info +--- +tags: + - Device +parameters: + - in: path + name: device_id + required: true + type: integer + description: Id of the device +responses: + 200: + description: Success + schema: + type: object + required: + - content + properties: + content: + $ref: '#/definitions/DeviceSecretInfo' diff --git a/app/devices/api.py b/app/devices/api.py index 260e2d7..1ad844d 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -2,6 +2,7 @@ import sys import hmac import urllib.parse import datetime +from secrets import token_urlsafe from .models import (Device, Recording, DeviceAssociation, @@ -150,6 +151,17 @@ def get_device(device_id): return Device.get(id=device_id) +def reset_device_secret(device_id): + """ + Resets device secret for device with given parameters. Raises error on + failure + """ + device = Device.get(id=device_id) + device.device_secret = token_urlsafe(32) + device.save() + return device + + def can_user_access_device(account_id, device_id): """ Checks if user with given account_id can access device with given device_id From 87cac36127a77c7dac9cb648ac6334835fe5ac6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 18:10:46 +0100 Subject: [PATCH 08/10] Add update device secret route --- app/api/resources/device.py | 13 +++++++- .../swagger/update_device_secret_spec.yaml | 26 ++++++++++++++++ app/devices/api.py | 30 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 app/api/resources/swagger/update_device_secret_spec.yaml diff --git a/app/api/resources/device.py b/app/api/resources/device.py index a53b133..3cad303 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -47,7 +47,7 @@ class RecordingsQuerySchema(Schema): class DeviceSecretSchema(BaseResourceSchema): device_secret = fields.String(dump_only=True) - secret_algorithm = fields.String() + secret_algorithm = fields.String(required=True) class DeviceShareSchema(BaseResourceSchema): @@ -173,6 +173,17 @@ class DeviceSecretResource(ProtectedResource): validate_device_ownership(device_id) return DeviceSecretSchema().dump(devices.get_device(device_id)), 200 + @use_args(DeviceSecretSchema(), locations=('json',)) + @swag_from('swagger/update_device_secret_spec.yaml') + def put(self, args, device_id): + validate_device_ownership(device_id) + return DeviceSecretSchema().dump( + devices.update_algorithm( + device_id, + args['secret_algorithm'] + ) + ), 200 + class DeviceSecretResetResource(ProtectedResource): @swag_from('swagger/reset_device_secret_spec.yaml') diff --git a/app/api/resources/swagger/update_device_secret_spec.yaml b/app/api/resources/swagger/update_device_secret_spec.yaml new file mode 100644 index 0000000..24f7e88 --- /dev/null +++ b/app/api/resources/swagger/update_device_secret_spec.yaml @@ -0,0 +1,26 @@ +Updates device secret info (algorithm) +--- +tags: + - Device +parameters: + - in: path + name: device_id + required: true + type: integer + description: Id of the device + - in: body + name: body + required: true + schema: + type: object + $ref: '#/definitions/DeviceSecretInfo' +responses: + 200: + description: Success + schema: + type: object + required: + - content + properties: + content: + $ref: '#/definitions/DeviceSecretInfo' diff --git a/app/devices/api.py b/app/devices/api.py index 1ad844d..8b5fb7f 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -2,6 +2,7 @@ import sys import hmac import urllib.parse import datetime +import hashlib from secrets import token_urlsafe from .models import (Device, Recording, @@ -155,6 +156,11 @@ def reset_device_secret(device_id): """ Resets device secret for device with given parameters. Raises error on failure + + :param device_id: Id of device + :type device_id: int + :returns: Requested device + :rtype: Device """ device = Device.get(id=device_id) device.device_secret = token_urlsafe(32) @@ -162,6 +168,30 @@ def reset_device_secret(device_id): return device +def update_algorithm(device_id, algorithm): + """ + Updates device secret algorithm for device with given parameters. Raises + error on failure + + :param device_id: Id of device + :type device_id: int + :param algorithm: Name of new algorithm + :type algorithm: string + :returns: Requested device + :rtype: Device + """ + if algorithm not in hashlib.algorithms_available: + raise ValueError("Unsupported algorithm! Supported algorithms: " + + str(hashlib.algorithms_available) + ". Some of " + + "these may not work on all platforms. These are " + + "guaranteed to work on every platform: " + + str(hashlib.algorithms_guaranteed)) + device = Device.get(id=device_id) + device.secret_algorithm = algorithm + device.save() + return device + + def can_user_access_device(account_id, device_id): """ Checks if user with given account_id can access device with given device_id From 162aa71a154b336940c13f0232b60ee69b2dc010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 18:11:52 +0100 Subject: [PATCH 09/10] Improve error handling on all routes --- app/api/blueprint.py | 16 ++++++++++++---- app/core.py | 4 ++-- requirements.txt | 1 - 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/api/blueprint.py b/app/api/blueprint.py index 25a543c..488e8ec 100644 --- a/app/api/blueprint.py +++ b/app/api/blueprint.py @@ -1,9 +1,11 @@ from flask_restful import Api from marshmallow import ValidationError -from flask import Blueprint +from flask import Blueprint, jsonify api_bp = Blueprint('api', __name__) + + api = Api(api_bp) @@ -79,12 +81,18 @@ add_resources() @api_bp.errorhandler(ValidationError) @api_bp.errorhandler(422) def handle_validation_error(e): - return {'status': 'error', 'message': str(e)}, 422 + return jsonify({'status': 'error', 'message': str(e)}), 422 + + +@api_bp.errorhandler(ValueError) +def handle_value_error(e): + return jsonify({'status': 'error', 'message': str(e)}), 422 @api_bp.errorhandler(Exception) +@api_bp.errorhandler(500) def handle_unknown_errors(e): - return ({ + return jsonify({ 'status': 'failed', 'message': 'Unknown error has occurred! ({0})'.format(str(e)) - }, 500) + }), 500 diff --git a/app/core.py b/app/core.py index 32002f2..bda72ff 100644 --- a/app/core.py +++ b/app/core.py @@ -1,5 +1,5 @@ # App initialization -from flask_api import FlaskAPI +from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_mail import Mail @@ -7,7 +7,7 @@ from flasgger import Swagger from flask_cors import CORS from .tasks import celery_configurator -app = FlaskAPI(__name__, instance_relative_config=True) +app = Flask(__name__, instance_relative_config=True) app.config.from_object('config') app.config.from_pyfile('config.py', silent=True) db = SQLAlchemy(app) diff --git a/requirements.txt b/requirements.txt index 0ccdb5a..171b2e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ cffi==1.11.5 click==6.7 flasgger==0.8.3 Flask==1.0.2 -Flask-API==1.0 Flask-Bcrypt==0.7.1 Flask-Cors==3.0.4 Flask-Mail==0.9.1 From 275c6d4c8700427a3110d8aacfc43732197bc658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Sat, 3 Nov 2018 18:14:11 +0100 Subject: [PATCH 10/10] Version 0.4.1 --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index acf60d4..e844357 100644 --- a/config.py +++ b/config.py @@ -2,7 +2,7 @@ import os # App configuration DEBUG = os.environ['DEBUG'] -APP_VERSION = '0.4.0' +APP_VERSION = '0.4.1' # Define the application directory BASE_DIR = os.path.abspath(os.path.dirname(__file__))