Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
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.
2 changes: 1 addition & 1 deletion api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys

from flask import Blueprint, jsonify
from models.user import db, UserProfile
from models import db, UserProfile
from flask_cors import CORS
from datetime import datetime, timezone

Expand Down
2 changes: 1 addition & 1 deletion api/goals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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

# Create the Blueprint for goals
Expand Down
2 changes: 1 addition & 1 deletion api/profile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# /api/profile.py
from flask import Blueprint, jsonify, request
from models.user import db, UserProfile
from models import db, UserProfile
from flask_cors import CORS

api = Blueprint('profile_api', __name__)
Expand Down
187 changes: 95 additions & 92 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,108 +1,111 @@
from flask import Flask, jsonify, session, render_template, request, redirect
import os
from flask import Flask, jsonify, session, render_template, redirect, url_for
from flask_cors import CORS
from flask_login import login_required, current_user
from dotenv import load_dotenv
from flask_wtf.csrf import CSRFError

# Local imports
from models import UserCredential
from extensions import db, login_manager, migrate, limiter, csrf
from api.routes import api
from api.goals import goals_bp
from api.profile import api as profile_api
from api.dashboard import dashboard_bp
from models import db
from dotenv import load_dotenv
import os
import pyrebase
from auth.routes import auth_bp

# Import scripts here
from scripts.add_default_user import add_default_user


# Load environment variables from .env file
load_dotenv()

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})

# Firebase configuration
config = {
'apiKey': os.getenv('FIREBASE_API_KEY'),
'authDomain': os.getenv('FIREBASE_AUTH_DOMAIN'),
'projectId': os.getenv('FIREBASE_PROJECT_ID'),
'storageBucket': os.getenv('FIREBASE_STORAGE_BUCKET'),
'messagingSenderId': os.getenv('FIREBASE_MESSAGING_SENDER_ID'),
'appId': os.getenv('FIREBASE_APP_ID'),
'measurementId': os.getenv('FIREBASE_MEASUREMENT_ID'),
'databaseURL': os.getenv('FIREBASE_DATABASE_URL')
}

firebase = pyrebase.initialize_app(config)
auth = firebase.auth()

# Flask config
app.secret_key = os.getenv("SECRET_KEY", "default_secret_key")
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///goals.db")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Initialize database
db.init_app(app)
with app.app_context():
db.create_all()

# Call function to create the default user
add_default_user()

# Register Blueprints
app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(goals_bp, url_prefix='/api/goals')
app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard')
app.register_blueprint(profile_api, url_prefix='/api/profile')

# Main index route (login + welcome)
@app.route('/', methods=['GET', 'POST'])
def index():
error = None
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
try:
user = auth.sign_in_with_email_and_password(email, password)
session['user'] = email
return redirect('/home')
except:
error = "Login failed. Please check your credentials."

return render_template('index.html', user=session.get('user'), error=error)

# Signup route
@app.route('/signup', methods=['GET', 'POST'])
def signup():
error = None
if request.method == 'POST':
email = request.form.get('email')
password = request.form.get('password')
try:
auth.create_user_with_email_and_password(email, password)
session['user'] = email
return redirect('/home')
except Exception as e:
error = "Signup failed. " + str(e).split("]")[-1].strip().strip('"')

return render_template('signup.html', error=error)

# Logout route
@app.route('/logout')
def logout():
session.pop('user', None)
return redirect('/')

@app.route('/home')
def home():
if 'user' in session:
return render_template('home.html', user=session['user'])
return redirect('/')

# Example API route
@app.route('/api/hello', methods=['GET'])
def hello():
return jsonify({'message': 'Hello from Flask!'}), 200
login_manager.login_view = "auth.login"

@login_manager.unauthorized_handler
def unauthorized():
return redirect(url_for('auth.login'))

@login_manager.user_loader
def load_user(user_id: str):
try:
return db.session.get(UserCredential, int(user_id))
except (TypeError, ValueError):
return None

def create_app():
app = Flask(__name__)

# Flask config (Core)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret")
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///goals.db")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config["WTF_CSRF_ENABLED"] = True

# Security settings for cookies
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
SESSION_COOKIE_SECURE=False,
REMEMBER_COOKIE_HTTPONLY=True,
REMEMBER_COOKIE_SECURE=False,
)


# Initialize database
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
if limiter:
limiter.init_app(app)
csrf.init_app(app)
CORS(
app,
resources={r"/api/*": {"origins": os.getenv("CORS_ORIGINS", "http://localhost:5173")}},
supports_credentials=True,
)

app.cli.add_command(add_default_user)

# Register Blueprints
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(api, url_prefix='/api')
app.register_blueprint(goals_bp, url_prefix='/api/goals')
app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard')
app.register_blueprint(profile_api, url_prefix='/api/profile')

@app.errorhandler(CSRFError)
def handle_csrf_error(e):
return f"CSRF failed: {e.description}", 400

# Routes
@app.route('/')
def index():
# send to login if not logged in
return redirect(url_for('auth.login'))

@app.route('/home')
@login_required
def home():
return render_template('home.html', user=current_user)

# Example API route
@app.route('/api/hello', methods=['GET'])
def hello():
return jsonify({'message': 'Hello from Flask!'}), 200

# Create database tables if they don't exist
with app.app_context():
db.create_all()

return app


if __name__ == '__main__':
debug_mode = os.getenv("FLASK_DEBUG", "False").lower() == "true"
app.run(debug=debug_mode, port=int(os.getenv("PORT", 5000)))
app = create_app()
debug = os.environ.get("FLASK_DEBUG") == "1"
port = int(os.getenv("PORT", 5000))
app.run(debug=debug, port=port)


110 changes: 110 additions & 0 deletions auth/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from flask import Blueprint, render_template, request, redirect, url_for
from flask_login import login_user, logout_user, login_required
from models import UserCredential, UserProfile
from urllib.parse import urlparse, urljoin
from extensions import db, limiter
from sqlalchemy.exc import IntegrityError

auth_bp = Blueprint('auth', __name__, template_folder='templates')

def validate_password(pwd: str):
if len(pwd) < 8:
raise ValueError("Password must be at least 8 characters long.")
if pwd.isdigit() or pwd.isalpha():
raise ValueError("Password must contain both letters and numbers.")
return True

def is_safe_url(target: str) -> bool:
if not target:
return False
ref = urlparse(request.host_url)
test = urlparse(urljoin(request.host_url, target))
return test.scheme in ('http', 'https') and ref.netloc == test.netloc

@auth_bp.route('/signup', methods=['GET', 'POST'])
def signup():
error = None
if request.method == 'POST':
name = request.form.get('name').strip()
email = request.form.get('email').strip().lower()
password = request.form.get('password')

try:
# Validate inputs
if not name or not email or not password:
raise ValueError("Name, email and password are required.")
validate_password(password)

# Check if email already exists
if UserCredential.query.filter_by(email=email).first():
error = "signup failed. email already registered."
return render_template('signup.html', error=error), 400


# Create user credentials
new_user = UserCredential(email=email)
new_user.set_password(password)
db.session.add(new_user)
db.session.flush() # Ensure new_user.id is available


profile = UserProfile(
user_id=new_user.id,
name=name,
account=email,
birthDate="---", #placeholder
gender="---", #placeholder
)
db.session.add(profile)
db.session.commit()

# Auto-login after signup
login_user(new_user)
return redirect(url_for("home"))

except ValueError as e:
db.session.rollback()
error = "Signup failed. " + str(e)
return render_template('signup.html', error=error), 400

except IntegrityError:
db.session.rollback()
error = "signup failed. email already registered."
return render_template('signup.html', error=error), 400

except Exception as e:
db.session.rollback()
error = "Signup failed. "+ str(e)
return render_template('signup.html', error=error), 400

return render_template('signup.html', error=error)

@auth_bp.route('/login', methods=['GET', 'POST'])
@limiter.limit("5 per minute", methods=['POST'], per_method=True) # Rate limiting to prevent brute-force attacks
def login():
error = None
if request.method == 'POST':
email = request.form.get('email').strip().lower()
password = request.form.get('password')
next_page = request.args.get('next') or request.form.get('next')
remember = bool(request.form.get('remember'))

# Loopkup user
user = UserCredential.query.filter_by(email=email).first()

#Auth check
if user and user.check_password(password):
login_user(user, remember=remember)
dest = next_page if is_safe_url(next_page) else url_for("home")
return redirect(dest)
else:
return "Login failed. Incorrect email or password.", 400

return render_template('login.html', error=None)

@auth_bp.route('/logout', methods=['GET'])
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))

Loading