From d1bd69b63f19994d810a61911f237852c4ec8d03 Mon Sep 17 00:00:00 2001 From: Raj Sarkar Date: Tue, 19 May 2026 18:31:50 +0530 Subject: [PATCH 1/2] Authentication, Google OAuth, UI fixes and alerts --- .gitignore | 3 +- app.py | 33 +++++++++- instance/database.db | Bin 0 -> 12288 bytes routes/auth_routes.py | 133 ++++++++++++++++++++++++++++++++++++++ routes/main_routes.py | 14 +++- static/script.js | 24 ++++++- static/style.css | 106 ++++++++++++++++++++++++++++++ templates/dashboard.html | 136 +++++++++++++++++++++++++++++++++++++++ templates/index.html | 9 +++ templates/login.html | 93 ++++++++++++++++++++++++++ templates/project.html | 7 ++ templates/signup.html | 92 ++++++++++++++++++++++++++ 12 files changed, 643 insertions(+), 7 deletions(-) create mode 100644 instance/database.db create mode 100644 routes/auth_routes.py create mode 100644 templates/dashboard.html create mode 100644 templates/login.html create mode 100644 templates/signup.html diff --git a/.gitignore b/.gitignore index b921615..7ab172d 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,5 @@ app.*.symbols # Python virtual environment venv/ .venv/ - +#Environment variables +.env diff --git a/app.py b/app.py index d7892bd..c2f6a43 100644 --- a/app.py +++ b/app.py @@ -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 ---- @@ -32,7 +60,8 @@ def internal_server_error(error): """Render a friendly 500 page for unexpected server errors.""" return render_template("500.html"), 500 - +with app.app_context(): + db.create_all() if __name__ == "__main__": # debug=True is only for local development. # Never run with debug=True in a production deployment. diff --git a/instance/database.db b/instance/database.db new file mode 100644 index 0000000000000000000000000000000000000000..9279cf75ee0c9448704573f38c187c574bf87945 GIT binary patch literal 12288 zcmeI$%SyvQ6b9g#cxww17Zz8-2nAb^lEfS0##n=`jj>Hr#Z?(iDzTSn(t6wJ(kJk( zd-QY>zL(3zaU@PbSm|x#qy)c3z@>A zGC8-Ir6lb8!A)}66>Iwz9}c3?xFx^8rOTsS*QccCv`DHx5z!D3fB*y_009U<00Izz z00bZa0SNrDK$}jI1@nHw<>#!y;|`BweqKyQJ!3^%*VeYq$TWF+9dAKK-sw(gWN;=ic+LPSGA g00Izz00bZa0SG_<0uX=z1R(Hl1?I=h^!IxQZ`omZ4*&oF literal 0 HcmV?d00001 diff --git a/routes/auth_routes.py b/routes/auth_routes.py new file mode 100644 index 0000000..90147fc --- /dev/null +++ b/routes/auth_routes.py @@ -0,0 +1,133 @@ +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") + ) + + +@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') + +@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') + +@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') + +@auth.route('/logout') +def logout(): + + session.pop('email', None) + flash("Logged out successfully.", "success") + return redirect('/login') + +#Google Login +@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" + ) + + +@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") \ No newline at end of file diff --git a/routes/main_routes.py b/routes/main_routes.py index 4cce046..09ee16b 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -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 @@ -31,6 +31,10 @@ def recommend(): interest (str) - Web | Data | Education | Automation | Games time (str) - Low | Medium | High """ + if 'email' not in session: + return jsonify({ + "error": "Please login to use DevPath." + }), 401 payload = request.get_json() if not payload: @@ -64,6 +68,8 @@ def recommend(): @main.route("/project/") 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) @@ -73,6 +79,10 @@ def project_detail(project_id): @main.route("/project//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 @@ -87,6 +97,8 @@ def view_code(project_id): @main.route("/project//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) diff --git a/static/script.js b/static/script.js index aebc922..ef4239e 100644 --- a/static/script.js +++ b/static/script.js @@ -434,7 +434,11 @@ if (isIndexPage) { headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) - .then(function (res) { return res.json(); }) + .then(function (res) { if(res.status===401){ + window.location.href="/login"; + return; + } + return res.json(); }) .then(function (data) { setLoadingState(false); if (data.error) { @@ -611,7 +615,10 @@ if (isDetailPage) { if (codeContentEl) codeContentEl.textContent = "Loading starter code..."; fetch("/project/" + PROJECT_ID + "/code") - .then(function (res) { return res.json(); }) + .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; @@ -743,4 +750,15 @@ function scrollToTop() { if (scrollTopBtn) { window.addEventListener('scroll', handleScroll); scrollTopBtn.addEventListener('click', scrollToTop); -} \ No newline at end of file +} +/*Flash Messages Auto Handle*/ +setTimeout(function () { + document.querySelectorAll(".flash-message").forEach(function (msg) { + msg.style.opacity = "0"; + msg.style.transform = "translateY(-10px)"; + + setTimeout(function () { + msg.remove(); + }, 300); + }); +}, 3000); \ No newline at end of file diff --git a/static/style.css b/static/style.css index 18d9cbf..9a72077 100644 --- a/static/style.css +++ b/static/style.css @@ -294,6 +294,24 @@ ul, ol { list-style: none; } transition: color var(--t); } +.nav-link-login { + border: 1px solid rgba(255,255,255,0.15); + padding: 8px 16px; + border-radius: 10px; +} + +.nav-link-signup { + background: linear-gradient(135deg, #7c3aed, #2563eb); + color: white !important; + padding: 8px 16px; + border-radius: 10px; + font-weight: 600; +} + +.nav-link-signup:hover { + transform: translateY(-1px); +} + /* Animated underline for mobile links */ .nav-mobile-link::after { content: ''; @@ -1968,4 +1986,92 @@ select:focus { #scroll-top-btn.visible { display: flex; +} +input { + width: 100%; + padding: 12px 14px; + border: 1.5px solid var(--border); + border-radius: var(--r-md); + font-size: 0.95rem; + font-family: inherit; + outline: none; + transition: 0.2s ease; + background: var(--white); +} + +input:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); +} +/*Google Login*/ +.google-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + + width: 100%; + padding: 12px; + + margin-top: 10px; + + border: 1px solid #ddd; + border-radius: 10px; + + text-decoration: none; + font-weight: 600; + color: #333; + background: #fff; + + transition: 0.2s ease; +} + +.google-btn:hover { + background: #f5f5f5; + transform: translateY(-1px); +} +/*Flash Messages*/ +.flash-container{ + width: min(900px, 92%); + margin: 100px auto 20px auto; + position: relative; + z-index: 999; +} + +.flash-message{ + padding: 16px 20px; + border-radius: 14px; + margin-bottom: 14px; + font-weight: 600; + font-size: 15px; + line-height: 1.5; + animation: fadeSlide 0.3s ease; + transition: all 0.3s ease; + + display: flex; + align-items: center; + gap: 10px; +} + +.flash-success{ + background: rgba(34,197,94,0.15); + color: #22c55e; + border: 1px solid rgba(34,197,94,0.35); +} + +.flash-error{ + background: rgba(239,68,68,0.15); + color: #ef4444; + border: 1px solid rgba(239,68,68,0.35); +} + +@keyframes fadeSlide{ + from{ + opacity: 0; + transform: translateY(-10px); + } + to{ + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..dc0291a --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,136 @@ + + + + + + Dashboard — DevPath + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} +{% endwith %} +
+
+
+ Your Dashboard +
+

+ Welcome, {{ user.name }} +

+

+ Manage your DevPath account and skills. +

+
+
+

+ Profile Information +

+

+ Name: + {{ user.name }} +

+

+ Email: + {{ user.email }} +

+
+
+

+ Account Status +

+

+ Logged in successfully. +

+

+ Your session will expire after 7 days of inactivity. +

+
+
+

+ Start Exploring +

+

+ Find coding projects tailored to your skills and interests. +

+ + Find Projects + +
+
+
+
+ + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 49556f7..c2d86af 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,6 +26,13 @@ Features Find Project GitHub + {% if session.get('email') %} + Dashboard + + {% else %} + Login + SignUp + {% endif %} + +
+ OR +
+ + + Google + Continue with Google + +

+ Don't have an account? + Create one +

+ + + + + + \ No newline at end of file diff --git a/templates/project.html b/templates/project.html index 21e3ae6..51604c5 100644 --- a/templates/project.html +++ b/templates/project.html @@ -19,6 +19,13 @@ diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..ceffcba --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,92 @@ + + + + + + Signup — DevPath + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
+
+
+ Join DevPath +
+

+ Create Your Account +

+

+ Save your skills, track projects, and personalize recommendations. +

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ {% if error %} +

+ {{ error }} +

+ {% endif %} + +
+

+ Already have an account? + Login +

+
+
+
+
+ + \ No newline at end of file From 76743dd22bfa36b6a3f1325433a5c10f5f605efd Mon Sep 17 00:00:00 2001 From: Raj Sarkar Date: Wed, 20 May 2026 22:52:12 +0530 Subject: [PATCH 2/2] Resolved conflicts --- routes/auth_routes.py | 9 ++++++--- routes/main_routes.py | 2 +- static/script.js | 4 +++- static/style.css | 7 +++++-- templates/index.html | 1 + templates/signup.html | 4 ++-- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 90147fc..e9cdadb 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -22,7 +22,7 @@ def check_password(self, password): self.password.encode("utf-8") ) - +#Signup route @auth.route('/signup', methods=['GET', 'POST']) def signup(): if request.method == 'POST': @@ -52,6 +52,7 @@ def signup(): return render_template('signup.html') +#Login Route @auth.route('/login', methods=['GET', 'POST']) def login(): @@ -72,6 +73,7 @@ def login(): return render_template('login.html') +#DashBoard Route @auth.route('/dashboard') def dashboard(): # Check if logged in @@ -86,6 +88,7 @@ def dashboard(): flash("Please login first.", "error") return redirect('/login') +#Logout Route @auth.route('/logout') def logout(): @@ -93,13 +96,13 @@ def logout(): flash("Logged out successfully.", "success") return redirect('/login') -#Google 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" + prompt="consent select_account"#ensure account selection during google login ) diff --git a/routes/main_routes.py b/routes/main_routes.py index 09ee16b..0ae9627 100644 --- a/routes/main_routes.py +++ b/routes/main_routes.py @@ -31,7 +31,7 @@ def recommend(): interest (str) - Web | Data | Education | Automation | Games time (str) - Low | Medium | High """ - if 'email' not in session: + 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 diff --git a/static/script.js b/static/script.js index ef4239e..b3dd4e8 100644 --- a/static/script.js +++ b/static/script.js @@ -434,6 +434,7 @@ if (isIndexPage) { headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }) + //means user not logged in/session expired .then(function (res) { if(res.status===401){ window.location.href="/login"; return; @@ -615,6 +616,7 @@ if (isDetailPage) { if (codeContentEl) codeContentEl.textContent = "Loading starter code..."; fetch("/project/" + PROJECT_ID + "/code") + //means user not logged in/session expired .then(function (res) {if(res.status===401){ window.location.href="/login"; return; @@ -751,7 +753,7 @@ if (scrollTopBtn) { window.addEventListener('scroll', handleScroll); scrollTopBtn.addEventListener('click', scrollToTop); } -/*Flash Messages Auto Handle*/ +/*Flash Messages Auto Handle to remove after a certain time*/ setTimeout(function () { document.querySelectorAll(".flash-message").forEach(function (msg) { msg.style.opacity = "0"; diff --git a/static/style.css b/static/style.css index 9a72077..81b0106 100644 --- a/static/style.css +++ b/static/style.css @@ -1987,6 +1987,7 @@ select:focus { #scroll-top-btn.visible { display: flex; } +/*Styling for input fields in login and signup pages*/ input { width: 100%; padding: 12px 14px; @@ -2003,7 +2004,8 @@ input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15); } -/*Google Login*/ + +/*Google Login styling*/ .google-btn { display: flex; align-items: center; @@ -2030,7 +2032,8 @@ input:focus { background: #f5f5f5; transform: translateY(-1px); } -/*Flash Messages*/ + +/*Flash Messages Styling*/ .flash-container{ width: min(900px, 92%); margin: 100px auto 20px auto; diff --git a/templates/index.html b/templates/index.html index c2d86af..8baf502 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,6 +26,7 @@ Features Find Project GitHub + {% if session.get('email') %} Dashboard diff --git a/templates/signup.html b/templates/signup.html index ceffcba..8bd72eb 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -57,7 +57,7 @@

@@ -66,7 +66,7 @@