forked from Monarch-Capstone-Parqe/CapstoneParqe
-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
440 lines (371 loc) · 14.2 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
import os
import subprocess
from urllib.parse import quote_plus, urlencode
from http import HTTPStatus
import smtplib
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, send_file, abort
import logging
from flask_session import Session
import jwt
from functools import wraps
from authlib.integrations.flask_client import OAuth
from werkzeug.exceptions import HTTPException
import database as db
from util import get_price, gen_file_uuid, process_order_data, send_email
import config.variables as variables
import fuse
# Init db
db.check_db_connect()
db.create_tables()
# Init flask
app = Flask(__name__, static_url_path='/static', static_folder='static')
app.config["SECRET_KEY"] = variables.APP_SECRET_KEY
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)
fuse.start_sending_orders()
# Init auth0
oauth = OAuth(app)
oauth.register(
"auth0",
client_id=variables.AUTH0_CLIENT_ID,
client_secret=variables.AUTH0_CLIENT_SECRET,
client_kwargs={
"scope": "openid profile email",
},
server_metadata_url=f'https://{variables.AUTH0_DOMAIN}/.well-known/openid-configuration',
)
# Configure the Flask app logger
app.logger.addHandler(logging.FileHandler("app.log"))
app.logger.setLevel(logging.INFO)
@app.route("/")
def user_home():
"""Render the homepage for users."""
return render_template("user/index.html")
def requires_auth(f):
"""Decorator to check if the staff is authenticated."""
@wraps(f)
def decorated(*args, **kwargs):
if 'token' not in session:
return redirect(url_for('staff_login'))
return f(*args, **kwargs)
return decorated
@app.route('/staff')
@requires_auth
def staff_home():
"""Render the homepage for staff members."""
return render_template("staff/index.html")
@app.route("/staff/callback", methods=["GET", "POST"])
def staff_callback():
"""Callback endpoint after successful staff authentication."""
token = oauth.auth0.authorize_access_token()
session["token"] = token
return redirect("/staff")
@app.route("/staff/login")
def staff_login():
"""Redirect users to Auth0 for login."""
return oauth.auth0.authorize_redirect(
redirect_uri=url_for("staff_callback", _external=True)
)
@app.route("/staff/logout")
@requires_auth
def staff_logout():
"""Logout the staff member."""
session.clear()
return redirect(
"https://"
+ variables.AUTH0_DOMAIN
+ "/v2/logout?"
+ urlencode(
{
"returnTo": url_for("staff_home", _external=True),
"client_id": variables.AUTH0_CLIENT_ID,
},
quote_via=quote_plus,
)
)
@app.route('/staff/verify', methods=['GET'])
@requires_auth
def staff_status():
"""Determine whether staff member is valid in database"""
try:
staff_email = session['token']['userinfo']['email']
result = db.staff_email_exists(staff_email)
return jsonify({"status": result}), HTTPStatus.OK
except Exception as e:
app.logger.error(f"Error in staff/status route: {e}")
return jsonify({'error': 'Internal Server Error'}), HTTPStatus.INTERNAL_SERVER_ERROR
@app.route('/order', methods=['POST'])
def order():
"""
Endpoint for users to upload model and preferences.
Returns:
JSON response indicating the status of the operation.
"""
try:
errors = process_order_data(request.form, session)
if errors:
return abort(HTTPStatus.BAD_REQUEST, f"Invalid/Incomplete input: {errors}")
# Save the model with a unique name in the uploads directory
_, ext = os.path.splitext(request.files.get('file').filename)
uuid = gen_file_uuid()
model_path = f'uploads/{uuid}{ext}'
gcode_path = f'uploads/{uuid}.gcode'
gcode_path_raw = f'{uuid}.gcode'
# Save the file to disk
request.files.get('file').save(model_path)
# Generate G-code
command = f'./prusa.AppImage --export-gcode {model_path} --layer-height {session["layer_height"]} --nozzle-diameter {session["nozzle_size"]} --infill-overlap {session["infill"]} --dont-arrange --load config/EPL_0.20mm_SPEED.ini'
prusa_output = subprocess.getoutput(command)
# Remove the original file
os.remove(model_path)
# Check if G-code was generated successfully
if not os.path.exists(gcode_path):
abort( HTTPStatus.BAD_REQUEST, jsonify(f"Failed to slice model. Check your file: {prusa_output}"))
price = get_price(gcode_path)
session['gcode_path'] = gcode_path_raw
session['price'] = price
session['prusa_output'] = prusa_output
# Send email
token = jwt.encode(payload={"email": session['email']}, key=app.config["SECRET_KEY"], algorithm="HS256")
verification_url = url_for('confirm_order', token=token, _external=True)
message = f"Press here to confirm your order: {verification_url}"
send_email(session['email'], "EPL Verify Order", message)
return jsonify('Model uploaded'), HTTPStatus.CREATED
# Reraise client errors
except HTTPException:
raise
# Email exceptions
except smtplib.SMTPException as e:
abort( HTTPStatus.BAD_REQUEST, jsonify(f"Error sending email: {e}"))
# Unkown
except Exception as e:
app.logger.critical(f"Error in order route: {e}")
abort(HTTPStatus.INTERNAL_SERVER_ERROR)
@app.route("/confirm_order/<token>")
def confirm_order(token):
"""
If this endpoint is visited with the correct token, the order is confirmed.
Args:
token: JWT token.
Returns:
JSON response indicating the status of the operation.
"""
try:
_ = jwt.decode(token, app.config["SECRET_KEY"], algorithms="HS256")
session['verified'] = True
# Store the order
db.insert_order(
session['email'],
session['filament_type'],
session['nozzle_size'],
session['layer_height'],
session['infill'],
session['quantity'],
session['note'],
session['prusa_output'],
session['gcode_path'],
session['price']
)
return redirect(url_for('order_confirmed'))
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token has expired"}), HTTPStatus.BAD_REQUEST
except jwt.InvalidTokenError as e:
return jsonify({"error": "Invalid token"}), HTTPStatus.BAD_REQUEST
@app.route("/order_confirmed")
def order_confirmed():
"""Render the order confirmation page."""
return render_template("order_confirmed.html")
@app.route('/staff/get_orders/<order_type>', methods=['GET'])
@requires_auth
def get_orders(order_type):
"""
Retrieve orders based on the specified type.
Args:
order_type: Type of orders to retrieve.
Returns:
JSON response containing the retrieved orders.
"""
if order_type == 'all':
orders = db.get_orders()
elif order_type == 'pending':
orders = db.get_pending_orders()
elif order_type == 'approved':
orders = db.get_approved_orders()
for order in orders:
order['approved_by'] = db.get_staff_email_by_approved_order_id(order['id'])
elif order_type == 'denied':
orders = db.get_denied_orders()
for order in orders:
order['denied_by'] = db.get_staff_email_by_denied_order_id(order['id'])
elif order_type == 'paid':
orders = db.get_paid_orders()
for order in orders:
order['checked_by'] = db.get_staff_email_by_paid_order_id(order['id'])
elif order_type == 'print':
orders = db.get_printing_orders()
elif order_type == 'closed':
orders = db.get_closed_orders()
else:
return jsonify({'error': 'Invalid order type'}), HTTPStatus.BAD_REQUEST
return jsonify({'orders': orders}), HTTPStatus.OK
@app.route('/staff/get_gcode/<gcode_path>', methods=['GET'])
@requires_auth
def get_gcode(gcode_path):
"""
Retrieve G-code.
Args:
gcode_path: Path to the G-code file.
Returns:
The G-code file.
"""
try:
search_path = f'uploads/{gcode_path}'
return send_file(search_path, as_attachment=True)
except FileNotFoundError:
return jsonify({'error': 'File not found'}), HTTPStatus.NOT_FOUND
except Exception as e:
app.logger.error(f"Error in get_gcode route: {e}")
return jsonify({'error': 'Internal Server Error'}), HTTPStatus.INTERNAL_SERVER_ERROR
@app.route('/staff/review_orders', methods=['PUT'])
@requires_auth
def review_orders():
"""
Review and update orders.
Returns:
JSON response indicating the status of the operation.
"""
try:
# Grab order details
order_id = request.form['id']
order_status = request.form['status']
order_email = db.get_email_by_order_id(order_id)
staff_email = session['token']['userinfo']['email']
if(order_status == 'denied'):
reason = request.form['message']
send_email(order_email, "EPL Order Denied", f"Reason: {reason}")
db.deny_order(order_id, staff_email)
elif(order_status == 'approved'):
db.approve_order(order_id, staff_email)
price = db.get_order_price(order_id)
send_email(order_email, "EPL Order Approved", f"To pay for your order, proceed to {variables.EPL_PAY_SITE} and navigate to the EPL Cashnet site via the 'checkout' button. Fill in your order price of " + str(price) + " in the 3D Printing checkout field and proceed through the payment steps.")
elif(order_status == 'confirm_payment'):
db.pay_order(order_id, staff_email)
else:
return abort(HTTPStatus.BAD_REQUEST, jsonify({'error': f'"{order_status}" is an invalid status.'}))
return jsonify({'message': 'Update received'}), HTTPStatus.OK
# Reraise client errors
except HTTPException:
raise
# Unkown
except Exception as e:
app.logger.critical(f"Error in order route: {e}")
abort(HTTPStatus.INTERNAL_SERVER_ERROR)
@app.route('/staff/filament/<action>', methods=['POST', 'PUT', 'DELETE'])
@requires_auth
def filament(action):
"""
Modify filaments based on the specified action.
Args:
action: Action to be carried out
"""
try:
filament_type = request.form['filament_type']
if action == 'add':
in_stock = request.form['in_stock']
db.add_filament(filament_type, in_stock)
black_id = db.get_color_id('black')
filament_id = db.get_filament_id(filament_type)
db.add_filament_color(filament_id, black_id)
elif action == 'update':
in_stock = request.form['in_stock']
db.update_filament(filament_type, in_stock)
elif action == 'remove':
db.remove_filament(filament_type)
elif action == 'add_color':
color_id = request.form['color_id']
filament_id = db.get_filament_id(filament_type)
db.add_filament_color(filament_id, color_id)
elif action == 'remove_color':
color_id = request.form['color_id']
filament_id = db.get_filament_id(filament_type)
db.remove_filament_color(filament_id, color_id)
else:
return jsonify({'error': 'Invalid action type'}), HTTPStatus.BAD_REQUEST
return jsonify({'message': 'Update received'}), HTTPStatus.OK
# Reraise client errors
except HTTPException:
raise
# Unkown
except Exception as e:
app.logger.critical(f"Error in filament route: {e}")
abort(HTTPStatus.INTERNAL_SERVER_ERROR)
@app.route('/staff/color/<action>', methods=['POST', 'DELETE'])
@requires_auth
def color(action):
"""
Modify colors based on the specified action.
Args:
action: Action to be carried out
"""
try:
if action == 'add':
color_name = request.form['color']
db.add_color(color_name)
elif action == 'remove':
db.remove_color(request.form['id'])
else:
return jsonify({'error': 'Invalid action type'}), HTTPStatus.BAD_REQUEST
return jsonify({'message': 'Update received'}), HTTPStatus.OK
# Reraise client errors
except HTTPException:
raise
# Unkown
except Exception as e:
app.logger.critical(f"Error in color route: {e}")
abort(HTTPStatus.INTERNAL_SERVER_ERROR)
@app.route('/staff/get_filament_inventory', methods=['GET'])
def get_filament_inventory():
"""
Retrieve filament inventory
Returns:
JSON response containing the retrieved filament types and associated colors
"""
try:
filaments = db.get_filaments()
filament_colors = {}
for each in filaments:
filament_colors.update({each['id']: db.get_filament_colors(each['id'])})
for each in filament_colors:
for i in filament_colors[each]:
i.update({'color': db.get_color(i['color_id'])})
colors = db.get_colors()
return jsonify({'filaments': filaments, 'filament_colors': filament_colors, 'colors': colors}), HTTPStatus.OK
# Reraise client errors
except HTTPException:
raise
# Unkown
except Exception as e:
app.logger.critical(f"Error in filament inventory route: {e}")
@app.route('/staff/close_order', methods=['PUT'])
@requires_auth
def close_order():
"""
Updates order status to completed or 'closed'.
Returns:
JSON response indicating the status of the operation.
"""
try:
# Grab order details
order_id = request.form['id']
db.close_order(order_id)
order_email = db.get_email_by_order_id(order_id)
send_email(order_email, "EPL Print Ready for Pickup", "Go get it.")
return jsonify({'message': 'Order closed'}), HTTPStatus.OK
# Unkown
except Exception as e:
app.logger.critical(f"Error in order route: {e}")
abort(HTTPStatus.INTERNAL_SERVER_ERROR)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
fuse.stop_sending_orders()