Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ app.*.symbols
# Python virtual environment
venv/
.venv/

#Environment variables
.env
36 changes: 33 additions & 3 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,40 @@

from flask import Flask, render_template
from routes.main_routes import main
from routes.auth_routes import auth, db
from datetime import timedelta
from authlib.integrations.flask_client import OAuth
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env")
import os

app = Flask(__name__)

#Session secret key
app.secret_key =os.getenv("SECRET_KEY")
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

#automatic logout after 7 days
app.permanent_session_lifetime = timedelta(days=7)

#Initialize db
db.init_app(app)
#Initialize OAuth
oauth = OAuth(app)
google = oauth.register(
name='google',
client_id=os.getenv("GOOGLE_CLIENT_ID"),
client_secret=os.getenv("GOOGLE_CLIENT_SECRET"),
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile'
}
)
app.extensions['google_auth'] = google
# Register all routes defined in the main Blueprint
app.register_blueprint(main)

app.register_blueprint(auth)

# ---- Error handlers ----

Expand All @@ -32,12 +60,14 @@ def internal_server_error(error):
"""Render a friendly 500 page for unexpected server errors."""
return render_template("500.html"), 500

auth-feature
with app.app_context():
db.create_all()
@app.errorhandler(405)
def method_not_allowed(error):
"""Render a friendly 405 page when the wrong HTTP method is used."""
return render_template("405.html"), 405


main
if __name__ == "__main__":
# debug=True is only for local development.
# Never run with debug=True in a production deployment.
Expand Down
Binary file added instance/database.db
Binary file not shown.
136 changes: 136 additions & 0 deletions routes/auth_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from flask import Blueprint, request, render_template, redirect, session, flash,current_app
from authlib.integrations.flask_client import OAuth
from flask_sqlalchemy import SQLAlchemy
import bcrypt

# Create Blueprint
auth = Blueprint("auth", __name__)

# Initialize Database
db = SQLAlchemy()

# User Model
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(100), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
# Password Check Method
def check_password(self, password):
return bcrypt.checkpw(
password.encode("utf-8"),
self.password.encode("utf-8")
)

#Signup route
@auth.route('/signup', methods=['GET', 'POST'])
def signup():
if request.method == 'POST':
name = request.form['name']
email = request.form['email']
password = request.form['password']
# Check if user already exists
existing_user = User.query.filter_by(email=email).first()
if existing_user:
flash("User already exists.", "error")
return render_template('signup.html')
# Hash password
hashed_password = bcrypt.hashpw(
password.encode('utf-8'),
bcrypt.gensalt()
).decode('utf-8')
# Create user
new_user = User(
name=name,
email=email,
password=hashed_password
)
db.session.add(new_user)
db.session.commit()
flash("Signup successful! Please login.", "success")
return redirect('/login')

return render_template('signup.html')

#Login Route
@auth.route('/login', methods=['GET', 'POST'])
def login():

if request.method == 'POST':
email = request.form['email']
password = request.form['password']
user = User.query.filter_by(email=email).first()
# Validate user
if user and user.check_password(password):
# Store session
session['email'] = user.email
session.permanent=True
flash("Logged in successfully!", "success")
return redirect('/dashboard')
else:
flash("Invalid email or password.", "error")
return render_template('login.html')

return render_template('login.html')

#DashBoard Route
@auth.route('/dashboard')
def dashboard():
# Check if logged in
if 'email' in session:
user = User.query.filter_by(
email=session['email']
).first()
return render_template(
'dashboard.html',
user=user
)
flash("Please login first.", "error")
return redirect('/login')

#Logout Route
@auth.route('/logout')
def logout():

session.pop('email', None)
flash("Logged out successfully.", "success")
return redirect('/login')

#Google Login Route
@auth.route('/login/google')
def login_google():
google = current_app.extensions['google_auth']
return google.authorize_redirect(
'http://127.0.0.1:5000/login/google/authorized',
prompt="consent select_account"#ensure account selection during google login
)


@auth.route('/login/google/authorized')
def google_authorized():
google = current_app.extensions['google_auth']

token = google.authorize_access_token()

user_info = token.get("userinfo")
if not user_info:
user_info = google.parse_id_token(token)

email = user_info["email"]
name = user_info["name"]

user = User.query.filter_by(email=email).first()

if not user:
user = User(
name=name,
email=email,
password="google-user"
)
db.session.add(user)
db.session.commit()

session["email"] = email

flash("Google login successful!", "success")
return redirect("/dashboard")
14 changes: 13 additions & 1 deletion routes/main_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Each route is kept thin: it validates input, calls a utility function,
# and returns a response. No business logic lives here.

from flask import Blueprint, render_template, request, jsonify, send_from_directory, abort
from flask import Blueprint, render_template, request, jsonify, send_from_directory, abort, redirect,session

from utils.recommender import get_recommendations, validate_recommendation_inputs
from utils.data_loader import find_project_by_id, get_project_stats
Expand Down Expand Up @@ -42,6 +42,10 @@ def recommend():
interest (str) - Web | Data | Education | Automation | Games
time (str) - Low | Medium | High
"""
if 'email' not in session: #done to ensure that only authenticated users get to use website features
return jsonify({
"error": "Please login to use DevPath."
}), 401
payload = request.get_json()

if not payload:
Expand Down Expand Up @@ -75,6 +79,8 @@ def recommend():
@main.route("/project/<int:project_id>")
def project_detail(project_id):
"""Render the full detail page for a single project."""
if 'email' not in session:
return redirect('/login')
project = find_project_by_id(project_id)
if not project:
abort(404)
Expand All @@ -84,6 +90,10 @@ def project_detail(project_id):
@main.route("/project/<int:project_id>/code")
def view_code(project_id):
"""Return the starter code file contents as JSON for inline display."""
if 'email' not in session:
return jsonify({
"error": "Unauthorized"
}), 401
project = find_project_by_id(project_id)
if not project:
return jsonify({"error": "Project not found."}), 404
Expand All @@ -98,6 +108,8 @@ def view_code(project_id):
@main.route("/project/<int:project_id>/download")
def download_code(project_id):
"""Serve the starter code file as a downloadable attachment."""
if 'email' not in session:
return redirect('/login')
project = find_project_by_id(project_id)
if not project:
abort(404)
Expand Down
26 changes: 25 additions & 1 deletion static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,16 @@ if (clearFiltersBtn) {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload) //convert object to json string
})
auth-feature
//means user not logged in/session expired
.then(function (res) { if(res.status===401){
window.location.href="/login";
return;
}
return res.json(); })

.then(function (res) { return res.json(); }) //parse the response as JSON
main
.then(function (data) {
setLoadingState(false);

Expand Down Expand Up @@ -751,7 +760,11 @@ if (isDetailPage) {
if (codeContentEl) codeContentEl.textContent = "Loading starter code...";

fetch("/project/" + PROJECT_ID + "/code")
.then(function (res) { return res.json(); })
//means user not logged in/session expired
.then(function (res) {if(res.status===401){
window.location.href="/login";
return;
} return res.json(); })
.then(function (data) {
if (data.error) {
if (codeContentEl) codeContentEl.textContent = "Error: " + data.error;
Expand Down Expand Up @@ -955,3 +968,14 @@ if (scrollTopBtn) {
window.addEventListener('scroll', handleScroll);
scrollTopBtn.addEventListener('click', scrollToTop);
}
/*Flash Messages Auto Handle to remove after a certain time*/
setTimeout(function () {
document.querySelectorAll(".flash-message").forEach(function (msg) {
msg.style.opacity = "0";
msg.style.transform = "translateY(-10px)";

setTimeout(function () {
msg.remove();
}, 300);
});
}, 3000);
Loading
Loading