diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..603f0b6 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,19 @@ +# This file specifies files that are *not* uploaded to Google Cloud +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +# Python pycache: +__pycache__/ +# Ignored by the build system +/setup.cfg \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6640694 --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Firebase key +firebase-key.json + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/ELEC60013-ES-CW1-Server.iml b/.idea/ELEC60013-ES-CW1-Server.iml new file mode 100644 index 0000000..02d2170 --- /dev/null +++ b/.idea/ELEC60013-ES-CW1-Server.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74962bf --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2164f7b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/api/authentication.py b/api/authentication.py new file mode 100644 index 0000000..0ad9b04 --- /dev/null +++ b/api/authentication.py @@ -0,0 +1,93 @@ +import json +import lib.utils +from flask import Response, Blueprint, request +from firebase_admin import firestore, auth +from firebase_admin._auth_utils import EmailAlreadyExistsError + +authentication = Blueprint('authentication', __name__) + +@authentication.route('/authentication/register', methods=['POST']) +def register(): + body = request.json + if body is None: + resp = {'error': 'Invalid request - please provide a body'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + email = body['email'] + password = body['password'] + name = body['name'] + deviceId = body['deviceid'] + + # Some fields are not present + if email is None or password is None or name is None or deviceId is None: + resp = {'error': 'Entries missing'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + # Register user with Firebase authentication + try: + user = auth.create_user( + email=email, + email_verified=False, + password=password, + display_name=name, + disabled=False) + except EmailAlreadyExistsError: + resp = {'error': 'User with given email address already exists'} + return Response(json.dumps(resp), status=409, mimetype='application/json') + # Prompt the user to get verified + code = lib.utils.saveVerificationCode(user.uid) + lib.utils.sendVerificationMail(name, email, code) + + # Link the user to the device + data = { + u'devices': [deviceId] + } + firestore.client().collection(u'devices').document(user.uid).set(data) + + # User successfully created and linked to device, return 201 + resp = {"uid": user.uid} + return Response(json.dumps(resp), status=201, mimetype='application/json') + +@authentication.route('/authentication/verify', methods=['POST']) +def verify(): + body = request.json + if body is None: + resp = {'error': 'Invalid request - please provide a body'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + uid = body['uid'] + code = body['code'] + + doc = firestore.client().collection(u'verification').document(uid).get() + if doc.exists: + if doc.to_dict()['code'] == code: + auth.update_user(uid, email_verified=True) + firestore.client().collection(u'verification').document(uid).delete() + resp = {'success': 'User verified'} + return Response(json.dumps(resp), status=200, mimetype='application/json') + else: + resp = {'error': 'Invalid code'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + else: + user = auth.get_user(uid) + code = lib.utils.saveVerificationCode(user.uid) + lib.utils.sendVerificationMail(user.display_name, user.email, code) + resp = {'error': 'Server could not find code, creating new one and sending email'} + return Response(json.dumps(resp), status=500, mimetype='application/json') + +@authentication.route('/authentication/get-user-devices', methods=['GET']) +def uploadReadings(): + uid = request.headers.get('UID') + if uid is None: + resp = {'error': 'UID not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + # Save all the measurements + doc = firestore.client().collection(u'devices').document(uid).get() + if doc.exists: + list = doc.to_dict()['devices'] + data = list + else: + data = [] + res = {'devices': data} + return Response(json.dumps(res), status=200, mimetype='application/json') diff --git a/api/data.py b/api/data.py new file mode 100644 index 0000000..629cf53 --- /dev/null +++ b/api/data.py @@ -0,0 +1,156 @@ +import time +import json +from datetime import datetime, time, timedelta +from flask import Response, Blueprint, request +from firebase_admin import firestore + +data = Blueprint('data', __name__) + +@data.route('/readings/save', methods=['POST']) +def uploadReadings(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + # Check that measurements are provided + body = request.json + if body is None: + resp = {'error': 'Invalid request - please provide a body'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + body['timestamp'] = datetime.now().timestamp() + + # Save all the measurements + doc = firestore.client().collection(u'readings').document(deviceId).get() + if doc.exists: + list = doc.to_dict()['data'] + list.append(body) + data = list + else: + data = [body] + upload = {'data': data} + firestore.client().collection(u'readings').document(deviceId).set(upload) + + resp = {'success': 'Data saved'} + return Response(json.dumps(resp), status=200, mimetype='application/json') + +@data.route('/readings/getall', methods=['GET']) +def getAllReadings(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + doc = firestore.client().collection(u'readings').document(deviceId).get() + if doc.exists: + data = doc.to_dict()['data'] + else: + data = [] + + results = {'data': data} + return Response(json.dumps(results), status=200, mimetype='application/json') + +@data.route('/readings/location/last', methods=['GET']) +def getLastLocation(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + doc = firestore.client().collection(u'readings').document(deviceId).get() + if doc.exists: + data = doc.to_dict()['data'] + lastEntry = data[-1] + lat = lastEntry['latitude'] + lon = lastEntry['longitude'] + else: + lat = -1.0 + lon = -1.0 + + results = {'latitude': lat, 'longitude': lon} + return Response(json.dumps(results), status=200, mimetype='application/json') + +@data.route('/readings/steps/today', methods=['GET']) +def getStepsToday(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + doc = firestore.client().collection(u'readings').document(deviceId).get() + if doc.exists: + data = doc.to_dict()['data'] + lastEntry = data[-1] + steps = lastEntry['cumulative_steps_today'] + else: + steps = 0 + + results = {'cumulative_steps_today': steps} + return Response(json.dumps(results), status=200, mimetype='application/json') + +@data.route('/readings/steps/last-five-days', methods=['GET']) +def getStepsLastFiveDays(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + upcomingMidnight = datetime.combine(datetime.today(), time.min) + timedelta(days=1) + doc = firestore.client().collection(u'readings').document(deviceId).get() + + if doc.exists: + data = doc.to_dict()['data'] + listOfDailySteps = [] + + for i in range(0, 5): + found = False + previousMidnight = upcomingMidnight - timedelta(days=1) + print(previousMidnight.timestamp()) + steps = 0 + for reading in reversed(data): + if reading['timestamp'] <= upcomingMidnight.timestamp() and reading['timestamp'] >= previousMidnight.timestamp() and not found: + steps = reading['cumulative_steps_today'] + found = True + listOfDailySteps.append(steps) + upcomingMidnight = previousMidnight + else: + listOfDailySteps = [0] * 5 + + results = {'daily_steps': listOfDailySteps} + return Response(json.dumps(results), status=200, mimetype='application/json') + +@data.route('/readings/metrics-summary', methods=['GET']) +def getMetricsSummary(): + deviceId = request.headers.get('Device-ID') + if deviceId is None: + resp = {'error': 'Device not specified'} + return Response(json.dumps(resp), status=400, mimetype='application/json') + + upcomingMidnight = datetime.combine(datetime.today(), time.min) + timedelta(days=1) + lastMidnight = datetime.combine(datetime.today(), time.min) + doc = firestore.client().collection(u'readings').document(deviceId).get() + + if doc.exists: + allData = doc.to_dict()['data'] + currentDayData = [x for x in allData if x['timestamp'] <= upcomingMidnight.timestamp() and x['timestamp'] >= lastMidnight.timestamp()] + if len(currentDayData) >= 1: + maxAirTemp = max(currentDayData, key=lambda x: x['air_temp'])['air_temp'] + maxSkinTemp = max(currentDayData, key=lambda x: x['skin_temp'])['skin_temp'] + maxHumidity = max(currentDayData, key=lambda x: x['humidity'])['humidity'] + minAirTemp = min(currentDayData, key=lambda x: x['air_temp'])['air_temp'] + minSkinTemp = min(currentDayData, key=lambda x: x['skin_temp'])['skin_temp'] + minHumidity = min(currentDayData, key=lambda x: x['humidity'])['humidity'] + results = { + 'last_air_temp': currentDayData[-1]['air_temp'], 'min_air_temp': minAirTemp, 'max_air_temp': maxAirTemp, + 'last_skin_temp': currentDayData[-1]['skin_temp'], 'min_skin_temp': minSkinTemp, 'max_skin_temp': maxSkinTemp, + 'last_humidity': currentDayData[-1]['humidity'], 'min_humidity': minHumidity, 'max_humidity': maxHumidity + } + return Response(json.dumps(results), status=200, mimetype='application/json') + else: + return Response(json.dumps({'error': 'Could not get data from database'}), status=500, mimetype='application/json') + else: + return Response(json.dumps({'error': 'Could not get data from database'}), status=500, mimetype='application/json') + + + + diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..a0b5f22 --- /dev/null +++ b/app.yaml @@ -0,0 +1 @@ +runtime: python38 \ No newline at end of file diff --git a/config/variables.py b/config/variables.py new file mode 100644 index 0000000..bf63e55 --- /dev/null +++ b/config/variables.py @@ -0,0 +1,9 @@ +# App config file with various variables and settings + +# Email config +MAIL_SERVER = 'smtp.gmail.com' +MAIL_PORT = 465 +MAIL_USERNAME = 'legbarkr@gmail.com' +MAIL_PASSWORD = '!Password123' +MAIL_USE_TLS = False +MAIL_USE_SSL = True \ No newline at end of file diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..2488e41 --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,26 @@ +from random import randint +from flask import current_app +from flask_mail import Mail, Message +from firebase_admin import firestore + +def sendMail(subject, sender, recipients, body): + mail = Mail(current_app) + msg = Message(subject, sender=sender, recipients=recipients) + msg.body = body + mail.send(msg) + +def saveVerificationCode(uid): + code = randint(100000, 999999) + data = { + u'code': code + } + firestore.client().collection(u'verification').document(uid).set(data) + return code + +def sendVerificationMail(name, email, code): + subject = 'Please verify your email for BarkFinder' + sender = 'legbarkr@gmail.com' + recipients = [email] + body = '''Hey {}! Thank you for signing up for BarkFinder. + In order to use our sevices, could you please verify your email address by logging in and entering this code {}'''.format(name, code) + sendMail(subject, sender, recipients, body) diff --git a/main.py b/main.py new file mode 100644 index 0000000..779d57e --- /dev/null +++ b/main.py @@ -0,0 +1,28 @@ +from config.variables import MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_USE_SSL, MAIL_USE_TLS +from flask import Flask +from firebase_admin import credentials, initialize_app +from api.authentication import authentication +from api.data import data + +# Initialize Flask app and register all the endpoints +app = Flask(__name__) +app.register_blueprint(authentication) +app.register_blueprint(data) + +# Initialize Mail instance +app.config['MAIL_SERVER'] = MAIL_SERVER +app.config['MAIL_PORT'] = MAIL_PORT +app.config['MAIL_USERNAME'] = MAIL_USERNAME +app.config['MAIL_PASSWORD'] = MAIL_PASSWORD +app.config['MAIL_USE_TLS'] = MAIL_USE_TLS +app.config['MAIL_USE_SSL'] = MAIL_USE_SSL + +# Initialize Firebase +firebase = initialize_app(credentials.Certificate('firebase-key.json')) + +@app.route('/') +def hello(): + return 'Hello World' + +if __name__ == '__main__': + app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9af04b9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +flask_mail +firebase_admin \ No newline at end of file