Skip to content

Commit 1f46d40

Browse files
Expressionless-Ball-ThingHanyuan Li
andauthored
Temp progress on the user profile routes (#60)
* feat: moved advent folder -> puzzles, added some comments * feat(docker): start separation of dev and prod builds, add pytest functionality to backend * feat(docker): added dev/prod to frontend, transition frontend to yarn * fix: remove .vscode folder * fix(makefile): restructured makefile a bit * feat: removed .vscode folder from git * feat(auth): get rudimentary autotesting in place, created clear_database function * feat(test): added all tests for auth/register * fix(puzzle): changed blueprint in routes/puzzle.py * Made some stub code for the user routes Made some stub code for the user routes. * Stub codes * feat(auth): refactored registration system, database connections * fix(auth): minor changes to constructor * feat(auth): implement email verification endpoints * feat(test): using fixtures * feat(auth): finish autotests, still needs commenting * feat(auth): finished writing tests for the most part * Merged stuff * user/stats should be working * node stuff * fixed up some stuff about node * profile, stat and set_name implemented * merged Co-authored-by: Hanyuan Li <[email protected]>
1 parent c58a660 commit 1f46d40

File tree

11 files changed

+1448
-203
lines changed

11 files changed

+1448
-203
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
config/*.env
2-
.vscode
2+
.vscode
3+
node_modules

backend/Pipfile.lock

Lines changed: 669 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
from auth.jwt import update_token
99
from common.plugins import jwt, mail
10+
from database.database import db
1011
from routes.auth import auth
1112
from routes.puzzle import puzzle
13+
from routes.user import user
14+
1215

1316
def handle_exception(error):
1417
response = error.get_response()
@@ -28,6 +31,9 @@ def create_app():
2831
app = Flask(__name__)
2932
CORS(app)
3033

34+
# Add database
35+
app.config["DATABASE"] = db
36+
3137
# Configure with all our custom settings
3238
app.config["JWT_SECRET_KEY"] = os.environ["FLASK_SECRET"]
3339

@@ -36,7 +42,7 @@ def create_app():
3642
app.config["JWT_BLACKLIST_ENABLED"] = True
3743
app.config["JWT_BLACKLIST_TOKEN_CHECKS"] = ["refresh"]
3844
app.config["JWT_COOKIE_CSRF_PROTECT"] = True
39-
app.config["JWT_TOKEN_LOCATION"] = "cookies"
45+
app.config["JWT_TOKEN_LOCATION"] = ["cookies"]
4046

4147
# TODO: convert to CSESoc SMTP server (if we have one) once we get that
4248
app.config["MAIL_SERVER"] = "smtp.mailtrap.io"
@@ -55,6 +61,7 @@ def create_app():
5561
# Register smaller parts of the API
5662
app.register_blueprint(auth, url_prefix="/auth")
5763
app.register_blueprint(puzzle, url_prefix="/puzzle")
64+
app.register_blueprint(user, url_prefix="/user")
5865

5966
# Register our error handler
6067
app.register_error_handler(HTTPException, handle_exception)

backend/auth/user.py

Lines changed: 144 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,144 @@
1-
from datetime import timedelta
2-
import random
3-
4-
from argon2 import PasswordHasher
5-
from argon2.exceptions import VerificationError
6-
from email_validator import validate_email, EmailNotValidError
7-
8-
from common.database import get_connection
9-
from common.exceptions import AuthError, InvalidError, RequestError
10-
from common.redis import cache
11-
12-
hasher = PasswordHasher(
13-
time_cost=2,
14-
memory_cost=2**15,
15-
parallelism=1
16-
)
17-
18-
class User:
19-
# TODO: change all these functions once database functions are merged
20-
21-
# Private helper methods
22-
@staticmethod
23-
def _email_exists(cursor, email):
24-
"""Checks if an email exists in the database."""
25-
cursor.execute("SELECT * FROM Users WHERE email = %s",
26-
(email,))
27-
28-
results = cursor.fetchall()
29-
return results != []
30-
31-
@staticmethod
32-
def _username_exists(cursor, username):
33-
"""Checks if a username is already used."""
34-
cursor.execute("SELECT * FROM Users WHERE username = %s", (username,))
35-
36-
results = cursor.fetchall()
37-
return results != []
38-
39-
@staticmethod
40-
def _add_user(conn, cursor, email, username, password):
41-
"""Given the details of a user, adds them to the database."""
42-
cursor.execute("INSERT INTO Users (email, username, password, numStars, score) VALUES (%s, %s, %s, 0, 0)",
43-
(email, username, password))
44-
conn.commit()
45-
46-
cursor.execute("SELECT uid FROM Users WHERE email = %s", (email,))
47-
id = cursor.fetchone()[0]
48-
49-
return id
50-
51-
# Constructor methods
52-
def __init__(self, email, password, id):
53-
self.email = email
54-
self.password = password
55-
self.id = id
56-
57-
# API-facing methods
58-
@staticmethod
59-
def register(email, username, password):
60-
"""Given an email, username and password, creates a verification code
61-
for that user in Redis such that we can verify that user's email."""
62-
# Error handling
63-
conn = get_connection()
64-
cursor = conn.cursor()
65-
66-
try:
67-
normalised = validate_email(email).email
68-
except EmailNotValidError as e:
69-
raise RequestError(description="Invalid email") from e
70-
71-
if User._email_exists(cursor, normalised):
72-
raise RequestError(description="Email already registered")
73-
74-
if User._username_exists(cursor, username):
75-
raise RequestError(description="Username already used")
76-
77-
hashed = hasher.hash(password)
78-
# TODO: remove addition of user to database
79-
new_id = User._add_user(conn, cursor, normalised, username, hashed)
80-
81-
cursor.close()
82-
conn.close()
83-
84-
# Add verification code to Redis cache, with expiry date of 1 hour
85-
code = random.randint(0, 999_999)
86-
data = {
87-
"code": f"{code:06}",
88-
"username": username,
89-
"password": hashed
90-
}
91-
92-
pipeline = cache.pipeline()
93-
94-
# We use a pipeline here to ensure these instructions are atomic
95-
pipeline.hset(f"register:{new_id}", mapping=data)
96-
pipeline.expire(f"register:{new_id}", timedelta(hours=1))
97-
98-
pipeline.execute()
99-
100-
return code
101-
102-
@staticmethod
103-
def login(email, password):
104-
"""Logs user in with their credentials (currently email and password)."""
105-
conn = get_connection()
106-
cursor = conn.cursor()
107-
108-
try:
109-
normalised = validate_email(email).email
110-
except EmailNotValidError as e:
111-
raise AuthError(description="Invalid email or password") from e
112-
113-
cursor.execute("SELECT * FROM Users WHERE email = %s", (normalised,))
114-
result = cursor.fetchone()
115-
116-
try:
117-
id, _, hashed = result
118-
hasher.verify(hashed, password)
119-
except (TypeError, VerificationError) as e:
120-
raise AuthError(description="Invalid email or password") from e
121-
122-
cursor.close()
123-
conn.close()
124-
125-
return User(normalised, hashed, id)
126-
127-
@staticmethod
128-
def get(id):
129-
"""Given a user's ID, fetches all of their information from the database."""
130-
conn = get_connection()
131-
cursor = conn.cursor()
132-
133-
cursor.execute("SELECT * FROM Users WHERE uid = %s", (id,))
134-
fetched = cursor.fetchall()
135-
136-
if fetched == []:
137-
raise InvalidError(description=f"Requested user ID {id} doesn't exist")
138-
139-
email, _, password, _, _ = fetched[0]
140-
141-
cursor.close()
142-
conn.close()
143-
144-
return User(email, password, id)
1+
from datetime import timedelta
2+
import random
3+
4+
from argon2 import PasswordHasher
5+
from argon2.exceptions import VerificationError
6+
from email_validator import validate_email, EmailNotValidError
7+
8+
from common.database import get_connection
9+
from common.exceptions import AuthError, InvalidError, RequestError
10+
from common.redis import cache
11+
12+
hasher = PasswordHasher(
13+
time_cost=2,
14+
memory_cost=2**15,
15+
parallelism=1
16+
)
17+
18+
class User:
19+
# TODO: change all these functions once database functions are merged
20+
21+
# Private helper methods
22+
@staticmethod
23+
def _email_exists(cursor, email):
24+
"""Checks if an email exists in the database."""
25+
cursor.execute("SELECT * FROM Users WHERE email = %s",
26+
(email,))
27+
28+
results = cursor.fetchall()
29+
return results != []
30+
31+
@staticmethod
32+
def _username_exists(cursor, username):
33+
"""Checks if a username is already used."""
34+
cursor.execute("SELECT * FROM Users WHERE username = %s", (username,))
35+
36+
results = cursor.fetchall()
37+
return results != []
38+
39+
@staticmethod
40+
def _add_user(conn, cursor, email, username, password):
41+
"""Given the details of a user, adds them to the database."""
42+
cursor.execute("INSERT INTO Users (email, username, password, numStars, score) VALUES (%s, %s, %s, 0, 0)",
43+
(email, username, password))
44+
conn.commit()
45+
46+
cursor.execute("SELECT uid FROM Users WHERE email = %s", (email,))
47+
id = cursor.fetchone()[0]
48+
49+
return id
50+
51+
# Constructor methods
52+
def __init__(self, email, password, id):
53+
self.email = email
54+
self.password = password
55+
self.id = id
56+
57+
# API-facing methods
58+
@staticmethod
59+
def register(email, username, password):
60+
"""Given an email, username and password, creates a verification code
61+
for that user in Redis such that we can verify that user's email."""
62+
# Error handling
63+
conn = get_connection()
64+
cursor = conn.cursor()
65+
66+
try:
67+
normalised = validate_email(email).email
68+
except EmailNotValidError as e:
69+
raise RequestError(description="Invalid email") from e
70+
71+
if User._email_exists(cursor, normalised):
72+
raise RequestError(description="Email already registered")
73+
74+
if User._username_exists(cursor, username):
75+
raise RequestError(description="Username already used")
76+
77+
hashed = hasher.hash(password)
78+
# TODO: remove addition of user to database
79+
new_id = User._add_user(conn, cursor, normalised, username, hashed)
80+
81+
cursor.close()
82+
conn.close()
83+
84+
# Add verification code to Redis cache, with expiry date of 1 hour
85+
code = random.randint(0, 999_999)
86+
data = {
87+
"code": f"{code:06}",
88+
"username": username,
89+
"password": hashed
90+
}
91+
92+
pipeline = cache.pipeline()
93+
94+
# We use a pipeline here to ensure these instructions are atomic
95+
pipeline.hset(f"register:{new_id}", mapping=data)
96+
pipeline.expire(f"register:{new_id}", timedelta(hours=1))
97+
98+
pipeline.execute()
99+
100+
return code
101+
102+
@staticmethod
103+
def login(email, password):
104+
"""Logs user in with their credentials (currently email and password)."""
105+
conn = get_connection()
106+
cursor = conn.cursor()
107+
108+
try:
109+
normalised = validate_email(email).email
110+
except EmailNotValidError as e:
111+
raise AuthError(description="Invalid email or password") from e
112+
113+
cursor.execute("SELECT * FROM Users WHERE email = %s", (normalised,))
114+
result = cursor.fetchone()
115+
116+
try:
117+
id, _, hashed = result
118+
hasher.verify(hashed, password)
119+
except (TypeError, VerificationError) as e:
120+
raise AuthError(description="Invalid email or password") from e
121+
122+
cursor.close()
123+
conn.close()
124+
125+
return User(normalised, hashed, id)
126+
127+
@staticmethod
128+
def get(id):
129+
"""Given a user's ID, fetches all of their information from the database."""
130+
conn = get_connection()
131+
cursor = conn.cursor()
132+
133+
cursor.execute("SELECT * FROM Users WHERE uid = %s", (id,))
134+
fetched = cursor.fetchall()
135+
136+
if fetched == []:
137+
raise InvalidError(description=f"Requested user ID {id} doesn't exist")
138+
139+
email, _, password, _, _ = fetched[0]
140+
141+
cursor.close()
142+
conn.close()
143+
144+
return User(email, password, id)

0 commit comments

Comments
 (0)