Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d07f46b
refector app.py to use pure flask long-in and register blueprint cleanly
Arita-pan Aug 26, 2025
6f59f15
implement signup, login and logout endpoints in auth/routes.py
Arita-pan Aug 26, 2025
2a0fe64
Define UserCredential model
Arita-pan Aug 26, 2025
62fb21c
rename User to UserProfile and add one-to-one relationship to UserCre…
Arita-pan Aug 26, 2025
7af1be3
update requirement.txt and rearrange
Arita-pan Aug 26, 2025
6b76dbd
Add base.html as shared layout
Arita-pan Aug 26, 2025
ea5c5ac
refactor login page
Arita-pan Aug 26, 2025
f01811c
create signup page with user registration form
Arita-pan Aug 26, 2025
3cd4944
create home page to display welcome message and logout option
Arita-pan Aug 26, 2025
3e946b1
update gitignore to keep repository clean from unnecessary files
Arita-pan Aug 26, 2025
696dba4
refactor API files to match model rename
Arita-pan Aug 26, 2025
dcda154
add CLI command to backfill UserProfile links to UserCredential
Arita-pan Aug 26, 2025
ccc8a08
update to match model rename
Arita-pan Aug 26, 2025
fba2bc5
add init.py to import UserCredential, UserProfile, and Goal
Arita-pan Aug 26, 2025
75327b1
update migration with latest schema changes
Arita-pan Aug 26, 2025
dd7969a
add username to signup page
Arita-pan Aug 27, 2025
4229b23
add csrf protection and remember me sessions
Arita-pan Aug 27, 2025
0928f5c
add session cookie and limiter for security
Arita-pan Aug 27, 2025
cf5869b
add password validation
Arita-pan Aug 27, 2025
d446330
fix signup/login flow and validation to pass all auth tests
Arita-pan Aug 27, 2025
cebffa9
add authentication documentation
Arita-pan Aug 27, 2025
c1d56ec
add more test for api-auth
Arita-pan Aug 27, 2025
b3fa0ba
update requirements
Arita-pan Aug 27, 2025
bfc0f6c
fix api-auth documentation remove POST logout section to only use GET
Arita-pan Aug 27, 2025
4aed423
fix CSRF errors and make logout work with GET
Arita-pan Aug 27, 2025
da15461
fix debug
Arita-pan Aug 29, 2025
1ef0930
fix security remove debug=True
Arita-pan Aug 29, 2025
a197f68
add password policy to docs
Arita-pan Sep 12, 2025
0f12587
handle missing fields and add proper validation
Arita-pan Sep 14, 2025
be0d15a
implement
Arita-pan Sep 14, 2025
21e7d37
add validation for goal updates and enforce owner-only deletion
Arita-pan Sep 15, 2025
ae26223
add POST /activity endpoint with validation for date and numeric fields
Arita-pan Sep 15, 2025
bb2967d
add end-to-end signup and login flow test
Arita-pan Sep 15, 2025
26f8764
add validation tests for posting activity data
Arita-pan Sep 15, 2025
b6ac827
verify time-sync returns server_time and reject bad requests
Arita-pan Sep 15, 2025
e8743d5
ensure protected routes required login (valid token)
Arita-pan Sep 15, 2025
f03f186
add profile tests (get self, update validation)
Arita-pan Sep 15, 2025
1713529
enforce owner check on PUT, return 403 for non-owners
Arita-pan Sep 15, 2025
802360b
add full test suite for create, list, update, delete with ownership a…
Arita-pan Sep 15, 2025
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
22 changes: 20 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
*.pyc
__pycache__/
env/
.env
.ipynb_checkpoints/


# Secrets
.env
.flaskenv

# Virutal Environments
/venv/
env/
.venv/

# Local Database
my_database.db

# Logs
*.logs
logs/

migrations/__pycache__/
migrations/versions/__pycache__/
Empty file added __init__.py
Empty file.
43 changes: 41 additions & 2 deletions api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
# alternatively, find edited files on planner board > 'Add Endpoint for the Dashboard'.
import sys

from flask import Blueprint, jsonify
from models.user import db, UserProfile
from flask import Blueprint, jsonify, request
from models import db, UserProfile
from flask_cors import CORS
from datetime import datetime, timezone
from flask_login import login_required, current_user
from extensions import csrf

# Create the Blueprint for dashboard
dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/api/dashboard')
Expand Down Expand Up @@ -38,3 +40,40 @@ def get_dashboard_data():
})

return jsonify({'message': 'User not found'}), 404


def _is_intlike(x):
try:
int(x)
return True
except Exception:
return False

@dashboard_bp.route('/activity', methods=['POST'])
@login_required
@csrf.exempt
def post_activity():
body = request.get_json(silent=True) or {}

# required field
date = body.get("date")
if not isinstance(date, str):
return jsonify({"error": "date is required (YYYY-MM-DD)"}), 400

# optional numeric fields; default to 0 if missing but validate type if present
numeric_fields = [
"steps",
"minutes_running",
"minutes_cycling",
"minutes_swimming",
"minutes_exercise",
"calories",
]
out = {"date": date, "user_id": getattr(current_user, "id", None)}
for f in numeric_fields:
v = body.get(f, 0)
if v != 0 and not _is_intlike(v):
return jsonify({"error": f"{f} must be an integer"}), 400
out[f] = int(v)

return jsonify(out), 201
40 changes: 32 additions & 8 deletions api/goals.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import Blueprint, request, jsonify
from models.goal import db, Goal # Correctly import db and Goal
from models import db, Goal # Correctly import db and Goal
from datetime import datetime
from flask_login import login_required, current_user

# Create the Blueprint for goals
goals_bp = Blueprint('goals', __name__, url_prefix='/api/goals')
Expand Down Expand Up @@ -37,6 +38,7 @@ def create_goal():

# Return the goal as a response with a 201 status
return jsonify(goal.as_dict()), 201

@goals_bp.route('/<int:user_id>', methods=['POST'])
def create_goal_for_user(user_id):
data = request.get_json()
Expand Down Expand Up @@ -80,33 +82,55 @@ def get_user_goals(user_id):

# Update Goal
@goals_bp.route('/<int:goal_id>', methods=['PUT'])
@login_required
def update_goal(goal_id):
data = request.get_json()
data = request.get_json() or {}

# Retrieve the goal with the specified goal_id
goal = Goal.query.get_or_404(goal_id)

if goal.user_id != current_user.id:
return jsonify({"error": "forbidden"}), 403

# Validation: dates must be ISO format (YYYY-MM-DD)
if "start_date" in data:
try:
goal.start_date = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
except ValueError:
return jsonify({"error": "Invalid start_date format, expected YYYY-MM-DD"}), 422

if "end_date" in data:
try:
goal.end_date = datetime.strptime(data["end_date"], "%Y-%m-%d").date()
except ValueError:
return jsonify({"error": "Invalid end_date format, expected YYYY-MM-DD"}), 422

# Loop through all the fields to update and check if they exist in the data
for field in ['start_date', 'end_date', 'steps', 'minutes_running', 'minutes_cycling',
'minutes_swimming', 'minutes_exercise', 'calories']:
if field in data:
# If it's a date field, convert it
if field in ['start_date', 'end_date']:
setattr(goal, field, datetime.strptime(data[field], '%Y-%m-%d').date())
else:
setattr(goal, field, data[field])
try:
val = int(data[field])
except (TypeError, ValueError):
return jsonify({"error": f"{field} must be a number"}), 422
if val < 0:
return jsonify({"error": f"{field} cannot be negative value"}), 422
setattr(goal, field, val)

# Commit the changes to the database
db.session.commit()

# Return the updated goal as a response
return jsonify(goal.as_dict())
return jsonify(goal.as_dict()), 200

# Delete Goal
@goals_bp.route('/<int:goal_id>', methods=['DELETE'])
def delete_goal(goal_id):
# Retrieve the goal by goal_id
goal = Goal.query.get_or_404(goal_id)

if goal.user_id != current_user.id:
return jsonify({"error": "forbidden"}), 403

# Delete the goal from the session
db.session.delete(goal)
Expand Down
90 changes: 64 additions & 26 deletions api/profile.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,89 @@
# /api/profile.py
from flask import Blueprint, jsonify, request
from models.user import db, UserProfile
from flask_login import login_required, current_user
from extensions import db, csrf
import re
from models import db, UserProfile
from flask_cors import CORS

api = Blueprint('profile_api', __name__)


# Profile Endpoints #

DATE_RX = re.compile(r"^\d{4}-\d{2}-\d{2}$")
# These routes are used by the frontend to fetch/update the user profile.
# Requires frontend request structure from ProfilePage.tsx

@api.route('', methods=['GET'])
@login_required
def get_profile():
# In future develpment get the user_id from a session or token
user_id = 1 # Replace with authenticated user ID
user = UserProfile.query.filter_by(id=user_id).first()
# user_id = 1 # Replace with authenticated user ID
# user = UserProfile.query.filter_by(id=user_id).first()

# if user:
# return jsonify({
# 'name': user.name,
# 'account': user.account,
# 'birthDate': user.birthDate,
# 'gender': user.gender,
# 'avatar': user.avatar
# })
# return jsonify({'message': 'User not found'}), 404

if user:
return jsonify({
'name': user.name,
'account': user.account,
'birthDate': user.birthDate,
'gender': user.gender,
'avatar': user.avatar
})
return jsonify({'message': 'User not found'}), 404
profile = UserProfile.query.filter_by(user_id=current_user.id).first()
if not profile:
return jsonify({"message": "User not found"}), 404

return jsonify({
"user_id": profile.user_id,
"name": profile.name,
"account": profile.account,
"birthDate": profile.birthDate,
"gender": profile.gender,
}), 200


@api.route('', methods=['POST'])
@login_required
@csrf.exempt
def update_profile():
data = request.get_json() # Get the JSON data sent from the frontend
data = request.get_json(silent=True) or {} # Get the JSON data sent from the frontend
errors = {}

if "name" in data:
if not isinstance(data["name"], str) or not data["name"].strip():
errors["name"] = "must be a non-empty string"

if "account" in data:
if not isinstance(data["account"], str) or not data["account"].strip():
errors["account"] = "must be a non-empty string"

if "birthDate" in data:
if not isinstance(data["birthDate"], str) or not DATE_RX.match(data["birthDate"]):
errors["birthDate"] = "use YYYY-MM-DD"

if "gender" in data:
if not isinstance(data["gender"], str):
errors["gender"] = "must be a string"

# In future develpment get the user_id from a session or token
user_id = 1 # Replace with authenticated user ID
user = UserProfile.query.filter_by(id=user_id).first()
if errors:
return jsonify({"error": "validation_error", "fields": errors}), 422

if not user:
return jsonify({'message': 'User not found'}), 404
profile = UserProfile.query.filter_by(user_id=current_user.id).first()
if not profile:
return jsonify({"message": "User not found"}), 404

# Update the user's profile with the new data
user.name = data['name']
user.account = data['account']
user.birthDate = data['birthDate']
user.gender = data['gender']
user.avatar = data.get('avatar', user.avatar) # Only update avatar if provided
# Only update fields that were provided
if "name" in data:
profile.name = data["name"].strip()
if "account" in data:
profile.account = data["account"].strip()
if "birthDate" in data:
profile.birthDate = data["birthDate"]
if "gender" in data:
profile.gender = data["gender"]

db.session.commit() # Save changes to the database

return jsonify({'message': 'Profile updated successfully'}), 200
return jsonify({'message': "Profile Updated Successfully", "user_id": profile.user_id}), 200
23 changes: 21 additions & 2 deletions api/routes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
# /api/routes.py
from flask import Blueprint, jsonify
from flask import Blueprint, jsonify, request
from flask_login import login_required
import time

api = Blueprint('api', __name__)

@api.route('/test', methods=['GET'])
def test_endpoint():
return jsonify({"message": "Hello, API!"})


@api.route("/time-sync", methods=["GET", "POST"])
@login_required # keep if you want it behind login
def time_sync():
# GET: simple “what time is it on the server?”
if request.method == "GET":
return jsonify({"server_time": int(time.time())}), 200

# POST: minimal schema check (adjust later as your design evolves)
body = request.get_json(silent=True) or {}
device_id = body.get("device_id")
readings = body.get("readings")
if not isinstance(device_id, str) or not isinstance(readings, list):
return jsonify({"error": "invalid request"}), 400

# TODO: store/process readings here if needed
return jsonify({"server_time": int(time.time())}), 200
Loading