Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit 0420a0e

Browse files
authored
Merge pull request #20 from codewizardshq/user-model-update
Landing Page + User model update
2 parents c156528 + a536815 commit 0420a0e

27 files changed

+3786
-72
lines changed

CodeChallenge/__init__.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
from flask import Flask, jsonify, make_response, send_from_directory
1+
import os
2+
3+
from flask import Flask, jsonify, make_response, send_from_directory, redirect
24
from flask_cors import CORS
35
from werkzeug.middleware.proxy_fix import ProxyFix
6+
from werkzeug.utils import import_string
47

8+
from . import core
59
from .api.eb import bp as eb_bp
610
from .api.questions import bp as questions_bp
711
from .api.users import bp as users_bp
@@ -27,8 +31,9 @@ def create_app(config):
2731
# prevent IP spoofing the rate limiter behind a reverse proxy
2832
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
2933

34+
cfg = import_string(f"CodeChallenge.config.{config}")()
3035
app.config.from_object(__name__)
31-
app.config.from_object(f"CodeChallenge.config.{config}")
36+
app.config.from_object(cfg)
3237

3338
# Initialize Plugins
3439
CORS(app)
@@ -53,29 +58,50 @@ def ratelimit_handler(e):
5358
status="error",
5459
reason=f"rate limit exceeded ({e.description})"), 429)
5560

61+
js_dir = os.path.join(app.config["DIST_DIR"], "js")
62+
css_dir = os.path.join(app.config["DIST_DIR"], "css")
63+
fonts_dir = os.path.join(app.config["DIST_DIR"], "fonts")
64+
images_dir = os.path.join(app.config["DIST_DIR"], "images")
65+
5666
@app.route("/js/<path:path>")
5767
def send_js(path):
58-
return send_from_directory("../dist/js", path)
68+
return send_from_directory(js_dir, path)
5969

6070
@app.route("/css/<path:path>")
6171
def send_css(path):
62-
return send_from_directory("../dist/css", path)
72+
return send_from_directory(css_dir, path)
6373

6474
@app.route("/fonts/<path:path>")
6575
def send_fonts(path):
66-
return send_from_directory("../dist/fonts", path)
76+
return send_from_directory(fonts_dir, path)
6777

6878
@app.route("/images/<path:path>")
6979
def send_images(path):
70-
return send_from_directory("../dist/images", path)
80+
return send_from_directory(images_dir, path)
7181

7282
@app.route("/assets/<path:path>")
7383
def send_assets(path):
7484
return send_from_directory("assets", path)
7585

86+
@app.route("/landing", defaults={"path": ""})
87+
@app.route("/landing/<path:path>")
88+
def send_landing(path):
89+
90+
if core.current_rank() != -1:
91+
return redirect("/")
92+
93+
if path:
94+
return send_from_directory("../landing/", path)
95+
return send_from_directory("../landing/", "index.html")
96+
7697
@app.route("/", defaults={"path": ""})
7798
@app.route("/<path:path>")
7899
def catch_all(path):
79-
return send_from_directory("../dist/", "index.html")
100+
101+
# show landing page
102+
if core.current_rank() == -1 and not path or path == "home":
103+
return redirect("/landing")
104+
105+
return send_from_directory(app.config["DIST_DIR"], "index.html")
80106

81107
return app

CodeChallenge/api/questions.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ def json_error(reason, status=400):
1515

1616

1717
@bp.route("/rank", methods=["GET"])
18-
@jwt_required
1918
def get_rank():
2019
return jsonify(status="success", rank=core.current_rank(),
21-
timeUntilNextRank=core.time_until_next_rank())
20+
maxRank=core.max_rank(),
21+
timeUntilNextRank=core.time_until_next_rank(),
22+
startsOn=core.friendly_starts_on())
2223

2324

2425
@bp.route("/next", methods=["GET"])
@@ -61,15 +62,13 @@ def answer_limit_attempts():
6162
return current_app.config.get("ANSWER_ATTEMPT_LIMIT", "3 per 30 minutes")
6263

6364

64-
# XXX: do we want to add a rate-limiter here?
65-
# https://flask-limiter.readthedocs.io/en/stable/
6665
@bp.route("/answer", methods=["POST"])
6766
@jwt_required
6867
@limiter.limit(answer_limit_attempts, key_func=user_rank)
6968
def answer_next_question():
7069
user = get_current_user()
7170
if core.current_rank() == user.rank:
72-
# all questions have been answered up to the corrent rank
71+
# all questions have been answered up to the current rank
7372
return jsonify({"status": "error",
7473
"reason": "no more questions to answer"}), 404
7574

CodeChallenge/api/users.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def login():
2626
username = request.json.get("username", None)
2727
password = request.json.get("password", None)
2828

29-
user = Users.query.filter_by(email=username).first()
29+
user = Users.query.filter_by(username=username).first()
3030
if user is None or not user.check_password(password):
3131
return json_error("invalid username or password")
3232

@@ -66,31 +66,38 @@ def register():
6666
user_data = request.get_json()
6767
new_u = Users()
6868

69-
email = user_data.get("email", None)
69+
# required fields first
70+
71+
parent_email = user_data.get("parentEmail", None)
7072
username = user_data.get("username", None)
73+
dob = user_data.get("DOB", None)
74+
password = user_data.get("password", None)
7175

72-
if email is None:
73-
return json_error("email is required")
76+
if parent_email is None:
77+
return json_error("parent email is required")
7478

7579
if username is None:
7680
return json_error("username is required")
7781

78-
password = user_data.get("password", None)
82+
if dob is None:
83+
return json_error("DOB is required")
7984

80-
if password is None or len(password) < 11 or len(password) > 120:
81-
return json_error("invalid password length (between 11 and 120)")
82-
83-
if Users.query.filter_by(email=email).first():
84-
return json_error("that email is already in use")
85+
if password is None or len(password) < 8 or len(password) > 120:
86+
return json_error("invalid password length (between 8 and 120)")
8587

8688
if Users.query.filter_by(username=username).first():
8789
return json_error("that username has been taken")
8890

89-
new_u.email = user_data['email']
90-
new_u.username = user_data['username']
91+
new_u.parent_email = parent_email
92+
new_u.username = username
9193
new_u.password = hash_password(password)
92-
new_u.firstname = user_data['firstname']
93-
new_u.lastname = user_data['lastname']
94+
95+
new_u.parentfirstname = user_data.get("parentFirstName")
96+
new_u.parentlastname = user_data.get("parentLastName")
97+
new_u.studentfirstname = user_data.get("studentFirstName")
98+
new_u.studentlastname = user_data.get("studentLastName")
99+
new_u.dob = dob
100+
94101
new_u.active = True
95102

96103
db.session.add(new_u)
@@ -106,11 +113,11 @@ def hello_protected():
106113
user = get_current_user()
107114

108115
return jsonify({"status": "success",
109-
"message": f"Hello {user.firstname}! (id {identity})",
116+
"message": f"Hello {user.studentfirstname}! (id {identity})",
110117
"username": user.username,
111-
"email": user.email,
112-
"firstname": user.firstname,
113-
"lastname": user.lastname,
118+
"email": user.parent_email,
119+
"firstname": user.studentfirstname,
120+
"lastname": user.studentfirstname,
114121
"rank": user.rank,
115122
"timeUntilNextRank": core.time_until_next_rank()})
116123

@@ -124,7 +131,7 @@ def forgot_password():
124131
if email is None:
125132
return jsonify(status="error", reason="email missing"), 400
126133

127-
user = Users.query.filter_by(email=email).first()
134+
user = Users.query.filter_by(parent_email=email).first()
128135

129136
if user is None:
130137
return jsonify(status="error",
@@ -138,7 +145,7 @@ def forgot_password():
138145
"did not make this request, you can ignore this email. "
139146
"To reset your password, use this link within 24 hours. "
140147
f"https://www.hackcwhq.com/reset-password?token={token}",
141-
recipients=[user.email])
148+
recipients=[user.parent_email])
142149

143150
if current_app.config.get("TESTING", False):
144151
msg.extra_headers = {"X-Password-Reset-Token": token}
@@ -159,8 +166,8 @@ def reset_password():
159166
if token is None or password is None:
160167
return json_error("missing token or password")
161168

162-
if len(password) < 11 and len(password) > 120:
163-
return json_error("invalid password length (between 11 and 120)")
169+
if 8 > len(password) > 120:
170+
return json_error("invalid password length (between 8 and 120)")
164171

165172
try:
166173
reset_password_from_token(token, password)

CodeChallenge/auth.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import datetime
2+
13
import argon2
24
from flask import current_app
35
from flask_jwt_extended import JWTManager
@@ -10,10 +12,16 @@
1012

1113
class Users(db.Model):
1214
id = db.Column(db.Integer, primary_key=True)
13-
firstname = db.Column(db.String(80), nullable=True)
14-
lastname = db.Column(db.String(80), nullable=True)
15+
studentfirstname = db.Column(db.String(80), nullable=True)
16+
studentlastname = db.Column(db.String(80), nullable=True)
17+
18+
parentfirstname = db.Column(db.String(80), nullable=True)
19+
parentlastname = db.Column(db.String(80), nullable=True)
20+
1521
username = db.Column(db.String(80), unique=True, nullable=False)
16-
email = db.Column(db.String(120), unique=True, nullable=False)
22+
parent_email = db.Column(db.String(120), unique=False, nullable=False)
23+
student_email = db.Column(db.String(120), unique=False, nullable=True)
24+
dob = db.Column(db.String(10), nullable=False)
1725
is_admin = db.Column(db.Boolean, nullable=True)
1826
password = db.Column(db.String(120), nullable=False)
1927
is_active = db.Column(db.Boolean, default=False, nullable=False)
@@ -36,39 +44,40 @@ def hash_password(plaintext):
3644

3745

3846
def authenticate(username, password):
39-
user = Users.query.filter_by(email=username).first()
47+
user = Users.query.filter_by(username=username).first()
4048
if user and user.check_password(password):
4149
return user
4250

4351

4452
@jwt.user_loader_callback_loader
45-
def identity(identity):
46-
return Users.query.get(identity)
53+
def identity(ident):
54+
return Users.query.get(ident)
4755

4856

4957
def create_user(email, username, password):
5058
u = Users()
51-
u.email = email
59+
u.parent_email = email
5260
u.username = username
5361
u.password = hash_password(password)
62+
u.dob = datetime.now().strftime("%Y-%m-%d")
5463

5564
db.session.add(u)
5665
db.session.commit()
5766

5867

5968
def reset_user(email, password):
60-
u = Users.query.filter_by(email=email)
69+
u = Users.query.filter_by(parent_email=email)
6170
u.password = hash_password(password)
6271

6372
db.session.commit()
6473

6574

6675
def password_reset_token(user: Users) -> str:
6776
ts = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
68-
return ts.dumps(user.email, salt="recovery-key")
77+
return ts.dumps(user.parent_email, salt="recovery-key")
6978

7079

7180
def reset_password_from_token(token: str, password: str):
7281
ts = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
73-
email = ts.loads(token, salt="recovery-key", max_age=86400)
74-
reset_user(email, password)
82+
parent_email = ts.loads(token, salt="recovery-key", max_age=86400)
83+
reset_user(parent_email, password)

CodeChallenge/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ProductionConfig(DefaultConfig):
4747
MAIL_SUPPRESS_SEND = False
4848
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
4949
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
50+
JWT_ACCESS_TOKEN_EXPIRES = 604800
5051

5152

5253
class DevelopmentConfig(ProductionConfig):

CodeChallenge/core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from datetime import datetime, timezone, timedelta, time
22

33
from flask import current_app
4+
from sqlalchemy import func
5+
6+
from .models import Question, db
47

58

69
def current_rank() -> int:
@@ -35,3 +38,15 @@ def time_until_next_rank() -> str:
3538
diff = datetime.combine(tomorrow, time.min, now.tzinfo) - now
3639

3740
return str(diff)
41+
42+
43+
def friendly_starts_on() -> str:
44+
"""Formatted specifically for the landing page countdown jQuery plugin"""
45+
epoch = int(current_app.config["CODE_CHALLENGE_START"])
46+
start = datetime.fromtimestamp(epoch, timezone.utc)
47+
48+
return start.strftime("%m/%d/%Y %H:%M%S UTC")
49+
50+
51+
def max_rank() -> int:
52+
return db.session.query(func.max(Question.rank)).scalar()

0 commit comments

Comments
 (0)