-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp.py
199 lines (145 loc) · 5.1 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
# Import framework
from requests import request, session
from flask import Flask, render_template, jsonify, session, redirect, abort, request
# SQL management
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.menu import MenuLink
from flask_admin.contrib.sqla import ModelView
# SQL serialising
from flask_marshmallow import Marshmallow
# Security stuff
import secrets
from dotenv import dotenv_values
from passlib.hash import bcrypt
from flask_wtf.csrf import CSRFProtect
# Ratelimiting
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# Setup bcrypt hasher
hasher = bcrypt.using(rounds=14) # Make bcrypt take longer (more secure)
# Load .env values
config = dotenv_values(".env")
# Error checking
if not all(key in config.keys() for key in ("MYSQL_USERNAME", "MYSQL_PASSWORD", "WEBAPP_USERNAME", "WEBAPP_PASSWORD")):
print("Environment variables not found. Remember to run setup.py")
exit()
# Create the application object
csrf = CSRFProtect()
app = Flask(__name__)
# Enable ratelimiting
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
csrf.init_app(app)
# Generate secure key for session
secret_key = secrets.token_hex()
app.config['SECRET_KEY'] = secret_key
# Configure SQLAlchemy options
app.config['SQLALCHEMY_DATABASE_URI'] = \
f'mysql://{config["MYSQL_USERNAME"]}:{config["MYSQL_PASSWORD"]}@localhost/classroom_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
ma = Marshmallow(app)
# Check MySQL server is running by sending a select request
db.engine.execute('SELECT 1')
# Create database model
class Classroom(db.Model):
id = db.Column(db.Integer, primary_key=True)
room_name = db.Column(db.String(50), nullable=False)
room_description = db.Column(db.String(280), nullable=True)
latitude = db.Column(db.DECIMAL(8, 6), nullable=False)
longitude = db.Column(db.DECIMAL(9, 6), nullable=False)
# This class is for serializing classroom_db to json
class ClassroomSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Classroom
load_instance = True
# Create SecureModelView for Classroom
class SecureModelView(ModelView):
def is_accessible(self):
if "logged_in" in session:
return True
else:
# Return 401 Unauthorised as session is not authenticated
abort(401)
# Create link for logging out
class LogoutMenuLink(MenuLink):
def is_accessible(self):
if "logged_in" in session:
return True
else:
abort(401)
# Initialise admin panel
admin = Admin(app, name='Admin Panel', template_mode='bootstrap3')
admin.add_view(SecureModelView(Classroom, db.session))
admin.add_link(LogoutMenuLink(name='Log Out', category='', url="/logout"))
# Creates a page to serve database as json. This is then read by the map.js file.
@app.route('/getpythondata')
def get_python_data():
classrooms = Classroom.query.all()
classroom_schema = ClassroomSchema(many=True)
output = classroom_schema.dump(classrooms)
return jsonify({'classroom': output})
# Use more secure HTTP headers
@app.after_request
def apply_caching(response):
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["HTTP-HEADER"] = "VALUE"
return response
# Pages
@app.route('/')
def home():
return render_template('index.j2')
@app.route('/guide.html')
def guide():
return render_template('guide.j2')
@app.route('/about.html')
def about():
return render_template('about.j2')
@app.route('/licensing.html')
def licensing():
return render_template('licensing.j2')
@app.route('/login', methods=['GET', 'POST'])
@limiter.limit("5 per minute")
def login():
# Redirect to admin panel if already logged in
if session.get('logged_in'):
return redirect("/admin")
if request.method == "POST":
# Verify username by checking against config and verify password with bcrypt
if request.form.get("username") == config["WEBAPP_USERNAME"] and \
hasher.verify(request.form.get("password"), config["WEBAPP_PASSWORD"]):
# Correct username and password, redirect to admin panel
session['logged_in'] = True
return redirect("/admin")
else:
# Incorrect username or password
return render_template("login.j2", failed=True)
return render_template("login.j2")
@app.route('/logout')
def logout():
session.clear()
return redirect('/')
# Error handling
@app.errorhandler(400)
def bad_request(e):
return render_template('errors/400.j2'), 400
@app.errorhandler(401)
def unauthorized(e):
# Do not return 401 error as it prevents redirecting
return redirect('/login')
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.j2'), 404
@app.errorhandler(429)
def too_many_requests(e):
return render_template('errors/429.j2'), 429
@app.errorhandler(500)
def server_error(e):
return render_template('errors/500.j2'), 500
# Start the server with the 'run()' method
if __name__ == '__main__':
app.run()