Skip to content

Commit 182d7e2

Browse files
authored
Merge pull request #71 from hcp-uw/Add_endpoint_documentation
Implemented change_user_email() and change_user_password() resources
2 parents f7f2d3c + 7ac4c4e commit 182d7e2

File tree

4 files changed

+106
-77
lines changed

4 files changed

+106
-77
lines changed

source/backend/model/error.py

+17
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@ class InvalidReceiptImage(Exception):
4343
class EdenAIBadRequest(Exception):
4444
pass
4545

46+
class MissingNewPassword(Exception):
47+
pass
48+
49+
class MissingNewEmail(Exception):
50+
pass
51+
52+
def handle_missing_new_password(error):
53+
response = jsonify({'error': 'Missing new password'})
54+
response.status_code = 400
55+
return response, response.status_code
56+
57+
def handle_missing_new_email(error):
58+
response = jsonify({'error': 'Missing new email'})
59+
response.status_code = 400
60+
return response, response.status_code
61+
62+
4663
def handle_edenai_bad_request(error):
4764
response = jsonify({'error': 'EdenAI bad request'})
4865
response.status_code = 500

source/backend/resource/resource_receipt.py

+28-30
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,44 @@
77
from config import Config
88
import requests
99
import os
10+
import uuid
11+
1012
receipts_bp = Blueprint('receipts', __name__)
1113

12-
"""
13-
Parses a receipt image and returns parsed receipt
14-
return the following:
15-
{
16-
'receipt_date': {date on receipt},
17-
'total': {total},
18-
'store': {store name},
19-
'location': {store address},
20-
'purchases': [{'name':{name of item},'price':{unit price},'amount':{amount/quantity}},
21-
{'name':{name of item},'price':{unit price},'amount':{amount/quantity}},
22-
{'name':{name of item},'price':{unit price},'amount':{amount/quantity}}
23-
...]
24-
}
25-
26-
"""
14+
2715
@receipts_bp.route('/receipts_parsing', methods=['POST'])
2816
@login_required
2917
def receipts_parsing():
18+
"""
19+
Parses a receipt image and returns parsed receipt
20+
return the following:
21+
{
22+
'receipt_date': {date on receipt},
23+
'total': {total},
24+
'store': {store name},
25+
'location': {store address},
26+
'purchases': [{'name':{name of item},'price':{unit price},'amount':{amount/quantity}},
27+
{'name':{name of item},'price':{unit price},'amount':{amount/quantity}},
28+
{'name':{name of item},'price':{unit price},'amount':{amount/quantity}}
29+
...]
30+
}
31+
"""
3032
receipt_image = request.files.get('receipt_image')
31-
if not receipt_image or receipt_image.filename == '':
33+
if not receipt_image:
3234
raise MissingReceiptImage()
3335

3436
# Create temp directory if it doesn't exist
3537
temp_dir = os.path.join(os.getcwd(), 'temp')
3638
os.makedirs(temp_dir, exist_ok=True)
3739

38-
# Save image into temp folder
39-
file_path = os.path.join(temp_dir, receipt_image.filename)
40+
# Create a unique filename for image that doesn't exist in temp
41+
while True:
42+
filename = f"{uuid.uuid4()}.jpg"
43+
file_path = os.path.join(temp_dir, filename)
44+
if not os.path.exists(file_path):
45+
break # Exit the loop if the filename is unique
46+
47+
# Save image into temp folder
4048
receipt_image.save(file_path)
4149

4250
# EdenAI API parameters
@@ -50,7 +58,7 @@ def receipts_parsing():
5058
try:
5159
with open(file_path, 'rb') as file:
5260
files = {'file': file}
53-
response = requests.post(url, data=data, files=files, headers=headers)
61+
response = requests.post(url, data=data, files=files, headers=headers) # EdenAI docs says accepts JPG, PNG, or PDF
5462
except:
5563
raise EdenAIBadRequest()
5664
finally:
@@ -64,7 +72,6 @@ def receipts_parsing():
6472
'location': result['amazon']['extracted_data'][0]['merchant_information']['address'],
6573
'purchases': []
6674
}
67-
6875
# {'name':{name of item},'price':{unit price},'amount':{amount/quantity}},{'name':{name of item},'price':{unit price},'amount':{amount/quantity}}
6976
items = result['amazon']['extracted_data'][0]['item_lines']
7077
for item in items:
@@ -82,16 +89,7 @@ def receipts_parsing():
8289
info['price'] = item['unit_price']
8390

8491
output['purchases'].append(info)
85-
print(output)
86-
return output
87-
########################################## ADD LOGIC HERE #################################
88-
89-
# See receipt_parsing.ipynb for sample calls to EdenAI and retrieving data
90-
91-
###########################################################################################
92-
# return jsonify({'message': 'to be implemented'}), 201
93-
return jsonify(result), 201
94-
92+
return jsonify(output), 201
9593

9694

9795
# Adds a receipt to the user

source/backend/resource/resource_user.py

+59-47
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,43 @@
99
# Define the blueprint
1010
auth_bp = Blueprint('auth', __name__)
1111

12+
### Delete email collection, no dependencies (duplicate data editing, causes consistency issues)
13+
### check user_id and email, shouldn't be too much more expensive since
14+
### subcollections don't automatically load
1215
@auth_bp.route('/login', methods = ['POST'])
1316
def login():
1417
db = current_app.db
1518
data = request.get_json()
16-
user_id = data.get('user_id')
19+
user_id = data.get('user_id') # either user_id or email
1720
password = data.get('password')
1821
if not user_id:
1922
raise MissingUserIDError()
2023
if not password:
2124
raise MissingPasswordError()
22-
user_ref = db.collection('Users').document(user_id).get() # checks for UserID
23-
if not user_ref.exists:
24-
user_ref = db.collection('UserEmails').document(user_id).get() # checks emails
25-
if not user_ref.exists:
26-
raise UserNotFound()
27-
user_id = user_ref.get('user_id')
28-
true_password_hash = user_ref.get('passwordHash')
29-
if not sha256_crypt.verify(password, true_password_hash):
30-
raise InvalidPassword()
31-
user = User(user_id)
32-
login_user(user)
33-
return jsonify({"message": f'{current_user.id} logged in successfully.'}), 200
25+
users_ref = db.collection('Users')
26+
docs = users_ref.stream()
27+
for doc in docs:
28+
user_id_db = doc.id
29+
email_db = doc.get('email')
30+
if user_id == user_id_db or user_id == email_db:
31+
true_password_hash = doc.get('passwordHash')
32+
if not sha256_crypt.verify(password, true_password_hash):
33+
raise InvalidPassword()
34+
else:
35+
user = User(user_id)
36+
login_user(user)
37+
return jsonify({"message": f'{current_user.id} logged in successfully.'}), 200
38+
raise UserNotFound()
3439

3540
@auth_bp.route('/logout', methods=['POST'])
3641
@login_required
3742
def logout():
3843
logout_user()
3944
return jsonify({"message": "Logged out successfully"}), 200
4045

41-
46+
### Delete email collection, no dependencies (duplicate data editing, causes consistency issues)
47+
### check user_id and email, shouldn't be too much more expensive since
48+
### subcollections don't automatically load
4249
# return email already exists error code 400
4350
@auth_bp.route('/register', methods=['POST'])
4451
def register():
@@ -60,21 +67,22 @@ def register():
6067
date = datetime.strptime(date_joined, '%Y-%m-%d')
6168
except:
6269
raise InvalidDateFormat()
63-
if db.collection('Users').document(user_id).get().exists:
64-
raise UserAlreadyExistsError()
65-
if db.collection('UserEmails').document(email).get().exists:
66-
raise EmailAlreadyExistsError()
70+
users_ref = db.collection('Users')
71+
docs = users_ref.stream()
72+
for doc in docs:
73+
existing_user_id = doc.id
74+
existing_email = doc.get('email')
75+
if user_id == existing_user_id:
76+
raise UserAlreadyExistsError()
77+
if email == existing_email:
78+
raise EmailAlreadyExistsError()
6779
passwordHash = sha256_crypt.hash(password)
6880
# use set to create new document with specified data (overwrites existing documents)
6981
db.collection('Users').document(user_id).set({
7082
'passwordHash': passwordHash,
7183
'email': email,
7284
'dateJoined' : date_joined
7385
})
74-
db.collection('UserEmails').document(email).set({
75-
'user_id':user_id,
76-
'passwordHash': passwordHash
77-
})
7886
user = User(user_id)
7987
login_user(user)
8088
return jsonify({"message": f'{current_user.id} logged in successfully.'}), 200
@@ -84,48 +92,52 @@ def get_user_info(user_id):
8492
user_ref = db.collection('Users').document(user_id).get()
8593
email = user_ref.get('email')
8694
date_joined = user_ref.get('dateJoined')
87-
return jsonify({"user_id": user_id, "email":email, "date_joined":date_joined}), 200
95+
return jsonify({"user_id": user_id, "email":email, "date_joined":date_joined})
8896

8997
@auth_bp.route('/user_info', methods=['GET'])
9098
@login_required
9199
def user_info():
92-
return get_user_info(current_user.id)
93-
100+
return get_user_info(current_user.id), 200
94101

95-
# input json includes 'old_email', 'new_email', old_user_id', 'new_user_id'
96-
@auth_bp.route('/change_user_info', methods=['POST'])
102+
### Resource for changing an email
103+
# input json includes 'new_email'
104+
@auth_bp.route('/change_user_email', methods=['POST'])
97105
@login_required
98-
def change_user_info():
106+
def change_user_email():
99107
db = current_app.db
100108
user_id = current_user.id
101109
data = request.get_json()
102-
new_user_id = data.get('new_user_id')
103110
new_email = data.get('new_email')
104-
if new_user_id != "":
105-
# In Users collection: create new document with {new_user_id} loaded with old user data, then delete old {user_id} document
106-
# see here: https://stackoverflow.com/questions/47885921/can-i-change-the-name-of-a-document-in-firestore
107-
# In UserEmails collection: update {user_id} field for the corresponding email
108-
user_id = new_user_id
109-
user = User(user_id) # create a new object with the new user_id
110-
login_user(user) # login user using new user_id
111-
pass
111+
if not new_email:
112+
raise MissingNewEmail()
112113
if new_email != "":
113-
# Get old {email} from {user_id} document
114-
# In UserEmails collection: create new document with {new_email} loaded with old {email} data, then delete old {email} document
115-
# In Users collection: find document {user_id} amd change the email to {new_email}
116-
pass
117-
return get_user_info(new_user_id)
114+
db.collection('Users').document(user_id).update({
115+
'email': new_email,
116+
})
117+
return jsonify({"message": "Email changed successfully."}), 201
118118

119-
# input json includes 'old_password', 'new_password'
119+
# input json includes 'current_password', 'new_password'
120120
@auth_bp.route('/change_user_password', methods=['POST'])
121121
@login_required
122122
def change_user_password():
123123
db = current_app.db
124124
user_id = current_user.id
125125
data = request.get_json()
126-
old_password = data.get('old_password')
126+
current_password = data.get('current_password')
127127
new_password = data.get('new_password')
128-
# In Users collection: find current user and get the {passwordHash}. Check if the hashed {old_password} matches {passwordHash} (similar to login endpoint).
129-
# If not matches, raise InvalidPassword()
130-
# If matches, hash the {new_password} and replace old value for {passwordHash} with new value for {passwordHash} on database.
128+
user_doc = db.collection('Users').document(user_id).get()
129+
if not current_password:
130+
raise MissingPasswordError()
131+
if not new_password:
132+
raise MissingNewPasswordError()
133+
true_password_hash = user_doc.get('passwordHash')
134+
if not sha256_crypt.verify(current_password, true_password_hash):
135+
raise InvalidPassword()
136+
newPasswordHash = sha256_crypt.hash(new_password)
137+
db.collection('Users').document(user_id).update({
138+
'passwordHash': newPasswordHash,
139+
})
140+
return jsonify({"message": 'Password changed successfully.'}), 201
141+
131142

143+
### FUTURE TODO: Reset password functionality

source/backend/server_main.py

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
(MissingUserDate, handle_missing_user_date),
4747
(MissingReceiptImage, handle_missing_receipt_image),
4848
(InvalidReceiptImage, handle_invalid_receipt_image),
49+
(MissingNewPassword, handle_missing_new_password),
50+
(MissingNewEmail, handle_missing_new_email),
4951
(Exception, handle_general_error)
5052
]
5153

0 commit comments

Comments
 (0)