Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Thumbs.db
!.gitignore
!.nvmrc
!.phpcs.xml.dist
!.github/

# dependencies
/.pnp
Expand Down
Empty file added backend/__init__.py
Empty file.
6 changes: 4 additions & 2 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from flask import request, jsonify, make_response
from config import app, db
from auth import (
from backend.config import app, db
from backend.auth import (
user_auth_register,
user_auth_login,
user_auth_logout,
user_auth_validate_session_token,
user_auth_validate_csrf_token,
)
from backend.classes.user import User
import re


Expand Down Expand Up @@ -160,6 +161,7 @@ async def validate_token():
if __name__ == "__main__":
with app.app_context():
db.create_all()
User.backup()

# Run Flask server in debug mode on port 4000 for local testing
app.run(host="0.0.0.0", port=4000, debug=True)
16 changes: 6 additions & 10 deletions backend/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from classes.user import User
from backend.classes.user import User
from flask import abort
import re
from data import users
from backend.data import users


def name_auth(name: str) -> bool:
Expand Down Expand Up @@ -67,13 +67,13 @@ def email_auth(email: str) -> bool:
400 Error: If email does not meet required format.
"""
# Regular expression for basic email validation.
email_pattern = r"^[0-9a-z]+([0-9a-z]*[-._+])*[0-9a-z]+@[0-9a-z]+([-.][0-9a-z]+)*([0-9a-z]*[.])[a-z]{2,6}$"
email_pattern = r"^[0-9a-z]+([0-9a-z]*[-._+])*[0-9a-z]+@[0-9a-z]+([-.][0-9a-z]+)*([0-9a-z]*[.])[a-z]{2,8}$"

if re.match(email_pattern, email) and 3 <= len(email) <= 255:
if re.match(email_pattern, email) and 3 <= len(email) <= 320:
return True
abort(
400,
description="Email must start with letters or numbers, include @ and a valid domain, have a 2-6 character extension, and be 3-320 characters long.",
description="Email must start with letters or numbers, include @ and a valid domain, have a 2-8 character extension, and be 3-320 characters long.",
)


Expand Down Expand Up @@ -146,11 +146,7 @@ async def user_auth_login(email: str, pwd_input: str) -> str:
safe_pwd = re.escape(pwd_input)

if safe_email not in users:
potential_user = User.from_email(safe_email)
if not potential_user:
abort(401, description="Email does not exist")
else:
users[safe_email] = potential_user
abort(401, description="Email does not exist")

user: User = users[safe_email]

Expand Down
Empty file added backend/classes/__init__.py
Empty file.
63 changes: 54 additions & 9 deletions backend/classes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from cryptography.fernet import Fernet
import secrets
import re
from db.user_db import UserDB
from config import db
from backend.db.user_db import UserDB
from backend.config import db
import os
from typing import Optional

Expand Down Expand Up @@ -55,18 +55,21 @@ def __init__(
self.set_csrf_token(self.generate_csrf_token())

@classmethod
def from_email(cls, user_email: str) -> Optional["User"]:
def backup(cls) -> dict["User"]:
"""
Loads an existing user from UserDB by user email.
Return a dictionary of existing user from UserDB.
"""
user_record = UserDB.query.filter_by(email=user_email).first()
user_record = UserDB.query.all()
if not user_record:
return None

user_obj = cls.__new__(cls) # Avoid calling __init__
user_obj.set_user_pk(user_record.id)
user_obj.set_session_token(user_obj.generate_session_token())
user_obj.set_csrf_token(user_obj.generate_csrf_token())
user_dict = {}
for user in user_record:
user_obj = cls.__new__(cls)
user_obj.set_user_pk(user.id)
user_obj.set_session_token(user_obj.generate_session_token())
user_obj.set_csrf_token(user_obj.generate_csrf_token())
user_dict[user.email] = user_obj

return user_obj

Expand Down Expand Up @@ -248,3 +251,45 @@ def get_last_name(self) -> str:
Returns the user's last name.
"""
return UserDB.query.filter_by(id=self.get_user_pk()).first().last_name

def get_email(self) -> str:
"""
Returns the user's email.
"""
return UserDB.query.filter_by(id=self.get_user_pk()).first().email

def get_password(self) -> str:
"""
Returns the user's password.
"""
return UserDB.query.filter_by(id=self.get_user_pk()).first().password

def set_password(self, new_pwd: str) -> None:
"""
Sets the user's password.
"""
UserDB.query.filter_by(id=self.get_user_pk()).first().password = new_pwd
db.session.commit()

def set_first_name(self, new_first_name: str) -> None:
"""
Sets the user's first name.
"""
UserDB.query.filter_by(id=self.get_user_pk()).first().first_name = (
new_first_name
)
db.session.commit()

def set_last_name(self, new_last_name: str) -> None:
"""
Sets the user's last name.
"""
UserDB.query.filter_by(id=self.get_user_pk()).first().last_name = new_last_name
db.session.commit()

def set_email(self, new_email: str) -> None:
"""
Sets the user's email.
"""
UserDB.query.filter_by(id=self.get_user_pk()).first().email = new_email
db.session.commit()
Empty file added backend/db/__init__.py
Empty file.
2 changes: 1 addition & 1 deletion backend/db/user_db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from config import db
from backend.config import db


class UserDB(db.Model):
Expand Down
5 changes: 3 additions & 2 deletions backend/flask.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ WORKDIR /app

COPY requirements.txt ./

RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

COPY . .

Expand All @@ -14,7 +14,8 @@ ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_RUN_PORT=4000
ENV FLASK_DEBUG=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app

EXPOSE 4000

CMD [ "python","-u","app.py" ]
CMD [ "python","-u","backend/app.py" ]
3 changes: 2 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ psycopg2-binary
Flask-SQLAlchemy
Flask-CORS
cryptography
asgiref
asgiref
pytest-asyncio
27 changes: 24 additions & 3 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ services:
environment:
- NEXT_PUBLIC_API_URL=http://localhost:4000/
volumes:
- ./frontend:/app
- ./frontend:/app/frontend
- /app/node_modules
depends_on:
- flaskapp
Expand All @@ -30,9 +30,9 @@ services:
- FLASK_ENV=development
- USER_FERNET_KEY=${USER_FERNET_KEY}
- SECRET_KEY=${SECRET_KEY}
command: ["python","app.py"]
command: ["python","-u", "backend/app.py"]
volumes:
- ./backend:/app
- ./backend:/app/backend
depends_on:
- db

Expand All @@ -49,5 +49,26 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data

# Test service to run pytest in the full environment
tests:
container_name: tests
build:
context: ./tests
dockerfile: tests.dockerfile
environment:
# Ensure absolute imports work by pointing PYTHONPATH to /app (the project root)
- PYTHONPATH=/app
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
- USER_FERNET_KEY=${USER_FERNET_KEY}
- SECRET_KEY=${SECRET_KEY}
volumes:
- ./backend:/app/backend
- ./tests:/app/tests
- ./.env:/app/.env
command: ["pytest", "tests"]
depends_on:
- db
- flaskapp

volumes:
pgdata: {}
Empty file added tests/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
flask
psycopg2-binary
Flask-SQLAlchemy
Flask-CORS
cryptography
asgiref
pytest-asyncio
Loading