Merge remote-tracking branch 'server/main'

This commit is contained in:
Aadi Desai 2022-03-07 23:30:35 +00:00
commit e507bb60b5
No known key found for this signature in database
GPG key ID: CFFFE425830EF4D9
15 changed files with 520 additions and 0 deletions

19
.gcloudignore Normal file
View file

@ -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

132
.gitignore vendored Normal file
View file

@ -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/

8
.idea/.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="Flask">
<option name="enabled" value="true" />
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/templates" />
</list>
</option>
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (ELEC60013-ES-CW1-Server)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ELEC60013-ES-CW1-Server.iml" filepath="$PROJECT_DIR$/.idea/ELEC60013-ES-CW1-Server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

93
api/authentication.py Normal file
View file

@ -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')

156
api/data.py Normal file
View file

@ -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')

1
app.yaml Normal file
View file

@ -0,0 +1 @@
runtime: python38

9
config/variables.py Normal file
View file

@ -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

26
lib/utils.py Normal file
View file

@ -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)

28
main.py Normal file
View file

@ -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()

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
flask
flask_mail
firebase_admin