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