diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 0000000..ca6478f --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,81 @@ +""" +Main +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2021 Project Author name guillermo.gutierrezmorote@concordia.ca +Project Collaborator name peteryefi@gmail.com +""" + +import flask +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin +from flask_apispec.extension import FlaskApiSpec +from flask_restful import Api +from hub_api.city_info import CityInfo, City +from hub_api.geometry import Geometry +from hub_api.greenery_catalog import GreeneryCatalogEntries +from hub_api.greenery_catalog import GreeneryCatalogEntry +from hub_api.greenery_catalog import GreeneryCatalogNames +from hub_api.construction_catalog import ConstructionCatalogEntries +from hub_api.construction_catalog import ConstructionCatalogEntry +from hub_api.construction_catalog import ConstructionCatalogNames +from hub_api.usage_catalog import UsageCatalogEntries +from hub_api.usage_catalog import UsageCatalogEntry +from hub_api.usage_catalog import UsageCatalogNames +from hub_api.heat_pump import HeatPump +from hub_api.lca import MaterialLCACatalog +from hub_api.lca import MaterialLCACalculations +from hub_api.construction import Construction +from hub_api.usage import Usage +from hub_api.energy_demand import EnergyDemand +from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive +from hub_api.uptime import Uptime +from hub_api.greenery import Greenery +from hub_api.user import User, UserLogin + +app = flask.Flask('gamification') +api = Api(app) + + +api.add_resource(Uptime, '/v1.4/uptime') +api.add_resource(Geometry, '/v1.4/geometry') +api.add_resource(GreeneryCatalogEntry, '/v1.4/greenery-catalog/entry') +api.add_resource(GreeneryCatalogEntries, '/v1.4/greenery-catalog/entries') +api.add_resource(GreeneryCatalogNames, '/v1.4/greenery-catalog/names') +api.add_resource(ConstructionCatalogEntry, '/v1.4/construction-catalog/entry') +api.add_resource(ConstructionCatalogEntries, '/v1.4/construction-catalog/entries') +api.add_resource(ConstructionCatalogNames, '/v1.4/construction-catalog/names') +api.add_resource(Construction, '/v1.4/construction') +api.add_resource(UsageCatalogEntry, '/v1.4/usage-catalog/entry') +api.add_resource(UsageCatalogEntries, '/v1.4/usage-catalog/entries') +api.add_resource(UsageCatalogNames, '/v1.4/usage-catalog/names') +api.add_resource(Usage, '/v1.4/usage') +api.add_resource(EnergyDemand, '/v1.4/energy-demand') +# api.add_resource(LCA, '/v1.4/lca') +api.add_resource(MaterialLCACatalog, '/v1.4/material_lca_catalog/entries') +api.add_resource(MaterialLCACalculations, '/v1.4/material_lca_catalog/calculations') +api.add_resource(HeatPump, '/v1.4/heat-pump') +api.add_resource(User, '/v1.4/user') +api.add_resource(UserLogin, '/v1.4/user/login') +api.add_resource(SessionStart, '/v1.4/session/start') +api.add_resource(SessionEnd, '/v1.4/session/end') +api.add_resource(KeepSessionAlive, '/v1.4/session/keep_alive') +api.add_resource(CityInfo, '/v1.4/city_info') +api.add_resource(City, '/v1.4/city') +api.add_resource(Greenery, '/v1.4/greenery') + +# Add api documentation +app.config.update({ + 'APISPEC_SPEC': APISpec( + title='Gamification Service', + version='v1.4', + plugins=[MarshmallowPlugin()], + openapi_version='2.0.0' + ), + 'APISPEC_SWAGGER_URL': '/swagger/', # URI to access API Doc JSON + 'APISPEC_SWAGGER_UI_URL': '/api-docs/' # URI to access UI of API Doc +}) +docs = FlaskApiSpec(app) +docs.register(HeatPump) +docs.register(User) +docs.register(UserLogin) +docs.register(City) diff --git a/gamification.py b/gamification.py index 6793dcc..7e41b5a 100644 --- a/gamification.py +++ b/gamification.py @@ -2,18 +2,9 @@ Main SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2021 Project Author name guillermo.gutierrezmorote@concordia.ca +Project Collaborator name peteryefi@gmail.com """ -import datetime -from pathlib import Path - -import flask -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from flask import Response -from flask_apispec.extension import FlaskApiSpec -from flask_restful import Api - from catalog_factories.construction_catalog_factory import ConstructionCatalogFactory from catalog_factories.greenery_catalog_factory import GreeneryCatalogFactory from catalog_factories.usage_catalog_factory import UsageCatalogFactory @@ -23,69 +14,11 @@ from imports.life_cycle_assessment_factory import LifeCycleAssessment from imports.schedules_factory import SchedulesFactory from imports.usage_factory import UsageFactory from imports.weather_factory import WeatherFactory -from hub_api.city_info import CityInfo -from hub_api.geometry import Geometry -from hub_api.greenery_catalog import GreeneryCatalogEntries -from hub_api.greenery_catalog import GreeneryCatalogEntry -from hub_api.greenery_catalog import GreeneryCatalogNames -from hub_api.construction_catalog import ConstructionCatalogEntries -from hub_api.construction_catalog import ConstructionCatalogEntry -from hub_api.construction_catalog import ConstructionCatalogNames -from hub_api.usage_catalog import UsageCatalogEntries -from hub_api.usage_catalog import UsageCatalogEntry -from hub_api.usage_catalog import UsageCatalogNames -from hub_api.heat_pump import HeatPump -from hub_api.lca import MaterialLCACatalog -from hub_api.lca import MaterialLCACalculations -from hub_api.construction import Construction -from hub_api.usage import Usage -from hub_api.energy_demand import EnergyDemand -from hub_api.session import SessionStart, SessionEnd, KeepSessionAlive -from hub_api.uptime import Uptime -from hub_api.greenery import Greenery +from flask import Response import hub_api.helpers.session_helper as sh - -app = flask.Flask('gamification') -api = Api(app) - - -api.add_resource(Uptime, '/v1.3/uptime') -api.add_resource(Geometry, '/v1.3/geometry') -api.add_resource(GreeneryCatalogEntry, '/v1.3/greenery-catalog/entry') -api.add_resource(GreeneryCatalogEntries, '/v1.3/greenery-catalog/entries') -api.add_resource(GreeneryCatalogNames, '/v1.3/greenery-catalog/names') -api.add_resource(ConstructionCatalogEntry, '/v1.3/construction-catalog/entry') -api.add_resource(ConstructionCatalogEntries, '/v1.3/construction-catalog/entries') -api.add_resource(ConstructionCatalogNames, '/v1.3/construction-catalog/names') -api.add_resource(Construction, '/v1.3/construction') -api.add_resource(UsageCatalogEntry, '/v1.3/usage-catalog/entry') -api.add_resource(UsageCatalogEntries, '/v1.3/usage-catalog/entries') -api.add_resource(UsageCatalogNames, '/v1.3/usage-catalog/names') -api.add_resource(Usage, '/v1.3/usage') -api.add_resource(EnergyDemand, '/v1.3/energy-demand') -# api.add_resource(LCA, '/v1.3/lca') -api.add_resource(MaterialLCACatalog, '/v1.3/material_lca_catalog/entries') -api.add_resource(MaterialLCACalculations, '/v1.3/material_lca_catalog/calculations') -api.add_resource(HeatPump, '/v1.3/heat-pump') -api.add_resource(SessionStart, '/v1.3/session/start') -api.add_resource(SessionEnd, '/v1.3/session/end') -api.add_resource(KeepSessionAlive, '/v1.3/session/keep_alive') -api.add_resource(CityInfo, '/v1.3/city_info') -api.add_resource(Greenery, '/v1.3/greenery') - -# Add api documentation -app.config.update({ - 'APISPEC_SPEC': APISpec( - title='Gamification Service', - version='v1.3', - plugins=[MarshmallowPlugin()], - openapi_version='2.0.0' - ), - 'APISPEC_SWAGGER_URL': '/swagger/', # URI to access API Doc JSON - 'APISPEC_SWAGGER_UI_URL': '/swagger-ui/' # URI to access UI of API Doc -}) -docs = FlaskApiSpec(app) -docs.register(HeatPump) +import datetime +from pathlib import Path +from bootstrap import app sh.begin_time = datetime.datetime.now() # initialize catalogs @@ -93,13 +26,11 @@ sh.greenery_catalog = GreeneryCatalogFactory('nrel').catalog sh.construction_catalog = ConstructionCatalogFactory('nrel').catalog sh.usage_catalog = UsageCatalogFactory('comnet').catalog - # Enrich the city data_path = (Path(__file__).parent / 'data').resolve() rihno_path = (Path(data_path / 'dompark.3dm')).resolve() city = GeometryFactory('rhino', rihno_path).city - for building in city.buildings: # Rihno files have no information about the function or the year of construction building.year_of_construction = 1995 @@ -135,6 +66,7 @@ for building in city.buildings: # Pass the city to the session helper to be used as default status. sh.city = city + @app.route("/") def home(): return Response(headers={'Access-Control-Allow-Origin': '*'}) diff --git a/hub_api/city_info.py b/hub_api/city_info.py index 0f5071c..30f29cd 100644 --- a/hub_api/city_info.py +++ b/hub_api/city_info.py @@ -4,11 +4,24 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2022 Project Author name guillermo.gutierrezmorote@concordia.ca """ import json - +from flask_apispec import use_kwargs, doc from flask import Response, request from flask_restful import Resource - from hub_api.helpers.session_helper import refresh_session +from marshmallow import fields, Schema +from hub_api.helpers.auth import role_required +from persistence.models import UserRoles +from flask_apispec.views import MethodResource +from hub_logger import logger + + +class AuthorizationHeader(Schema): + Authorization = fields.Str(required=True, description='Authorization token') + AppID = fields.Str(required=True, description='ID of app accessing API') + + +class CitySchema(Schema): + city_file = fields.Raw(type='file', required=True, description='City file') class CityInfo(Resource): @@ -70,3 +83,16 @@ class CityInfo(Resource): 'buildings': buildings } return Response(json.dumps(response), headers=headers) + + +class City(MethodResource, Resource): + @doc(description='Persist a city', tags=['PersistCity']) + @role_required(UserRoles.Admin.value) + @use_kwargs(AuthorizationHeader, location='headers') + @use_kwargs(CitySchema) + def post(self, **kwargs): + try: + return Response(response=json.dumps({'msg': 'Hello'}), status=201) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400) diff --git a/hub_api/helpers/auth.py b/hub_api/helpers/auth.py new file mode 100644 index 0000000..fa23250 --- /dev/null +++ b/hub_api/helpers/auth.py @@ -0,0 +1,73 @@ +from datetime import datetime, timedelta, timezone +from typing import Dict +from jwt import JWT, jwk_from_pem +import os +from jwt.utils import get_int_from_datetime +from functools import wraps +from flask import request, g +from hub_logger import logger +from persistence.models import UserRoles +from jwt.exceptions import JWTException + +instance = JWT() + + +def generate_auth_token(user: Dict): + private_key = "{}/rsa.pem".format(os.path.expanduser('~')) + with open(private_key, 'rb') as fh: + signing_key = jwk_from_pem(fh.read()) + user['exp'] = get_int_from_datetime(datetime.now(timezone.utc) + timedelta(hours=24)) + user['iat'] = get_int_from_datetime(datetime.now(timezone.utc)) + return instance.encode(user, signing_key, alg='RS256') + + +def validate_auth_token(token: str): + public_key = "{}/rsa.pub".format(os.path.expanduser('~')) + with open(public_key, 'rb') as fh: + verifying_key = jwk_from_pem(fh.read()) + return instance.decode(token, verifying_key, do_time_check=True) + + +def role_required(role: str): + def auth_module(user): + g.user = user + return user['role'] == role + + """ + A wrapper to authorize specific roles for specific endpoints + :param roles: a list of roles allowed + :return: + """ + + def role_decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + try: + token = request.headers['Authorization'] + user = validate_auth_token(token) + + if user is None: + return {'messages': 'You have not been authenticated'}, 401 + allowed = auth_module(user['user']) + + if user['user']['role'] == UserRoles.Admin.value and 'localhost' not in request.headers['Host']: + allowed = False + + if not allowed: + return {'messages': 'You are not authorized'}, 403 + return f(*args, **kwargs) + except KeyError as err: + # logger Error + logger.error(err) + return {'messages': 'Invalid payload'}, 400 + except JWTException as err: + logger.error(err) + return {'messages': 'Invalid token'}, 401 + except Exception as err: + logger.error(err) + return {'messages': 'Sorry, an error occurred while processing your request'}, 500 + + return wrapper + + return role_decorator + diff --git a/hub_api/user.py b/hub_api/user.py new file mode 100644 index 0000000..1a3beb8 --- /dev/null +++ b/hub_api/user.py @@ -0,0 +1,113 @@ +""" +HeatPump Service +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Project Author Peter Yefi peteryefi@gmail.com +""" +import json +from flask import Response +from flask_apispec import use_kwargs, doc +from flask_apispec.views import MethodResource +from flask_restful import Resource +from marshmallow import Schema, fields +from imports.user_factory import UserFactory +from exports.user_factory import UserFactory as ExUserFactory +import os +from hub_logger import logger +from hub_api.helpers.auth import generate_auth_token, role_required +from persistence.models import UserRoles + + +class AuthorizationHeader(Schema): + Authorization = fields.Str(required=True, description='Authorization token') + AppID = fields.Str(required=True, description='ID of app accessing API') + + +class LoginPostSchema(Schema): + """ + Defines post data for users + """ + password = fields.String(required=True, description='Password of user') + email = fields.String(required=True, description='Email of user') + + +class UserPostSchema(LoginPostSchema): + """ + Defines post data for users + """ + name = fields.String(required=True, description='Name of user') + role = fields.String(required=True, description='Allowed user roles', enum=['Admin', 'Hub_Reader']) + + +class UserPutSchema(UserPostSchema): + """ + Defines put data for users + """ + id = fields.Int(required=True, description='The Id of the user to be Updated') + + +class User(MethodResource, Resource): + def __init__(self): + self.user_factory = UserFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + @doc(description='Create users', tags=['CreateUser']) + @role_required(UserRoles.Admin.value) + @use_kwargs(AuthorizationHeader, location='headers') + @use_kwargs(UserPostSchema) + def post(self, **kwargs): + try: + + user = self.user_factory.create_user(name=kwargs["name"], email=kwargs["email"], password=kwargs["password"], + role=kwargs["role"]) + if type(user) is dict: + return Response(response=json.dumps(user), status=400) + return Response(response=json.dumps({'user': {'id': user.id, 'name': user.name, 'email': user.email, + 'password': user.password, 'role': user.role.value}}), status=201) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while creating user'}), status=400) + + @doc(description='Get all users', tags=['UpdateUsers']) + @use_kwargs(UserPutSchema) + @role_required(UserRoles.Admin.value) + @use_kwargs(AuthorizationHeader, location='headers') + def put(self, **kwargs): + try: + res = self.user_factory.update_user(user_id=kwargs['id'], name=kwargs['name'], password=kwargs['password'], + role=kwargs['role'], email=kwargs['email']) + if res: + return Response(response=json.dumps(res), status=400) + return Response(response=json.dumps({'success': 'user updated successfully'}), status=200) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry, an error occurred while updating user'}), + status=400) + + +class UserLogin(MethodResource, Resource): + def __init__(self): + self.user_factory = ExUserFactory(db_name='hub_prod', app_env='PROD', + dotenv_path="{}/.env".format(os.path.expanduser('~'))) + + @doc(description='Create users', tags=['LoginUser']) + @use_kwargs(LoginPostSchema) + def post(self, **kwargs): + try: + user = self.user_factory.login_user(email=kwargs["email"], password=kwargs["password"]) + if type(user) is dict: + return Response(response=json.dumps(user), status=400) + user = user[0] + user_dict = { + 'user': { + 'id': user.id, + 'name': user.name, + 'email': user.email, + 'password': user.password, + 'role': user.role.value, + } + } + user_dict['token'] = generate_auth_token(user_dict) + return Response(response=json.dumps(user_dict), status=201) + except Exception as err: + logger.error(err) + return Response(response=json.dumps({'err_msg': 'Sorry an error occurred while authenticating user'}), status=400) diff --git a/requirements.txt b/requirements.txt index c519ccc..5a2fa80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,5 @@ ply rhino3dm==7.7.0 scipy PyYAML -pyecore==0.12.2 \ No newline at end of file +pyecore==0.12.2 +jwt==1.3.1 \ No newline at end of file