From 6ebd1e4cc009a16e718e82595189b907441b0d48 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Fri, 26 Jun 2026 16:52:07 +0530 Subject: [PATCH 1/5] Feat: Migrate Data Source from JSON to Relational Database (SQLAlchemy) (#1176) - Added Flask-SQLAlchemy to requirements - Created Project db.Model with db.JSON support - Wrote seed_db.py script to migrate JSON to SQLite - Refactored load_all_projects to query SQLite db - Updated test_basic.py to use temporary in-memory sqlite db --- data/devpath.db | Bin 0 -> 36864 bytes requirements.txt | 3 ++- src/app.py | 4 ++++ src/config.py | 5 ++++ src/models.py | 38 +++++++++++++++++++++++++++++++ src/seed_db.py | 48 +++++++++++++++++++++++++++++++++++++++ src/utils/data_loader.py | 35 +++++++--------------------- tests/test_basic.py | 38 +++++++++++++++++++++++++++++++ 8 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 data/devpath.db create mode 100644 src/models.py create mode 100644 src/seed_db.py diff --git a/data/devpath.db b/data/devpath.db new file mode 100644 index 0000000000000000000000000000000000000000..c1523376c2d8ff15c893df544a2d77dfa940026f GIT binary patch literal 36864 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 Date: Fri, 26 Jun 2026 17:06:03 +0530 Subject: [PATCH 2/5] Feat: Implement User Authentication via GitHub OAuth (#1177) - Integrated Authlib for GitHub OAuth login flow - Created User SQLAlchemy database model - Created auth routes (/login, /authorize, /logout) - Developed protected /profile dashboard template - Hooked session auth state into main navigation - Added automated routing redirection tests --- data/devpath.db | Bin 0 -> 12288 bytes requirements.txt | 5 +- src/app.py | 37 +++++++ src/config.py | 12 +++ src/models.py | 25 +++++ src/routes/auth_routes.py | 56 ++++++++++ src/routes/main_routes.py | 30 +++++- src/templates/index.html | 14 +++ src/templates/profile.html | 206 +++++++++++++++++++++++++++++++++++++ tests/test_basic.py | 26 +++++ 10 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 data/devpath.db create mode 100644 src/models.py create mode 100644 src/routes/auth_routes.py create mode 100644 src/templates/profile.html diff --git a/data/devpath.db b/data/devpath.db new file mode 100644 index 0000000000000000000000000000000000000000..b420adbede94190bdca335bdf3f5ed8b745a7e0c GIT binary patch literal 12288 zcmeI#%}&BV5C`xr8kHt|+#DqKfZ?dzQ?sU@q^|bw7$4@+Z45JB8=#*8NB(ZZM#+a1Ny=*Rr$oH2?wu*e) zkl4xhqEuP1LV3>0^UBY*7qAZj2tWV=5P$##AOHafKmY;|*bw-vNQa7|NM9cb9|c|- zdr_Q+Ld!CA+aO!N=opkgB2_N>F11bDxHK#>yEd7FPUo29;**~|rz0oZz1OYQm2Ro^ zMon8Q5iO>C;{8?BHY)gxCp>b}C|K1rnoUjgkNM1Vop|gAK}^^EuDRBF6o%sokH%Ta zYZShC56M48cUJpWw}y0M3`zCfyzRhj_XY;3tIyYDO)DHIXNtu9sq4MRZ$WmmcX*nF zdG6#8PCZ}5z5YmemPDwEfB^vrKmY;|fB*y_009U<00Izzz^)3&;$XP{@9OWxxgY=m V2tWV=5P$##AOHafKmY={z%R^`Xp#T` literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt index 22936867..e1e08a5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,7 @@ Flask==3.0.3 # Testing (optional but recommended for contributors) pytest==8.2.2 -flake8==7.0.0 \ No newline at end of file +flake8==7.0.0 +Authlib==1.3.1 +requests==2.32.3 +Flask-SQLAlchemy==3.1.1 diff --git a/src/app.py b/src/app.py index 62873074..160349ae 100644 --- a/src/app.py +++ b/src/app.py @@ -20,12 +20,39 @@ from routes.main_routes import main from config import Config from errors.handlers import register_error_handlers +from models import db +from authlib.integrations.flask_client import OAuth +from flask import session app = Flask(__name__) # Load config settings into Flask's internal config manager properly app.config.from_object(Config) +# Initialize SQLAlchemy +db.init_app(app) + +with app.app_context(): + db.create_all() + +# Initialize OAuth +oauth = OAuth(app) +github = oauth.register( + name='github', + client_id=app.config.get("GITHUB_CLIENT_ID"), + client_secret=app.config.get("GITHUB_CLIENT_SECRET"), + access_token_url='https://github.com/login/oauth/access_token', + access_token_params=None, + authorize_url='https://github.com/login/oauth/authorize', + authorize_params=None, + api_base_url='https://api.github.com/', + client_kwargs={'scope': 'read:user'}, +) + +# Register blueprints +from routes.auth_routes import auth_bp +app.register_blueprint(auth_bp, url_prefix='/auth') + # Register all routes defined in the main Blueprint (This handles your '/' route!) app.register_blueprint(main) @@ -34,6 +61,16 @@ # so Blueprint-level error handlers take precedence where defined. register_error_handlers(app) +@app.context_processor +def inject_user(): + """Make current_user available to all templates.""" + user_id = session.get('user_id') + current_user = None + if user_id: + from models import User + current_user = db.session.get(User, user_id) + return dict(current_user=current_user) + @app.after_request def add_security_headers(response): diff --git a/src/config.py b/src/config.py index 8cea9d05..f8b4a79d 100644 --- a/src/config.py +++ b/src/config.py @@ -13,6 +13,18 @@ class Config: # Can be overridden via environment variable for different deployments BASE_URL = os.getenv("BASE_URL", "https://mydevpath-github.vercel.app") + # Security and Session + SECRET_KEY = os.getenv("SECRET_KEY", "dev_secret_key_change_in_production") + + # GitHub OAuth Settings + GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "dummy_client_id") + GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "dummy_client_secret") + + # Database Settings + basedir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///" + os.path.join(basedir, "data", "devpath.db")) + SQLALCHEMY_TRACK_MODIFICATIONS = False + # Application metadata for OG tags SITE_NAME = "DevPath" SITE_DESCRIPTION = "Get personalized coding project recommendations with step-by-step roadmaps and starter code." diff --git a/src/models.py b/src/models.py new file mode 100644 index 00000000..e42bb414 --- /dev/null +++ b/src/models.py @@ -0,0 +1,25 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + github_id = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(100), nullable=False) + avatar_url = db.Column(db.String(255), nullable=True) + + # Store arrays of strings as JSON + saved_skills = db.Column(db.JSON, nullable=False, default=list) + bookmarked_projects = db.Column(db.JSON, nullable=False, default=list) + + def to_dict(self): + return { + "id": self.id, + "github_id": self.github_id, + "username": self.username, + "avatar_url": self.avatar_url, + "saved_skills": self.saved_skills if self.saved_skills else [], + "bookmarked_projects": self.bookmarked_projects if self.bookmarked_projects else [] + } diff --git a/src/routes/auth_routes.py b/src/routes/auth_routes.py new file mode 100644 index 00000000..69ffdb80 --- /dev/null +++ b/src/routes/auth_routes.py @@ -0,0 +1,56 @@ +from flask import Blueprint, session, redirect, url_for, current_app +from models import db, User + +# We import github from app, but since it's instantiated there, it might cause circular import. +# Instead, we can import it locally or place the blueprint accordingly. +# A better way is to pass github from app, or just import it. +# To avoid circular imports, let's delay import or import github from the current_app context or from a dedicated module. +# For simplicity, we can do from app import github + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login') +def login(): + from app import github + # generate a redirect uri + redirect_uri = url_for('auth.authorize', _external=True) + return github.authorize_redirect(redirect_uri) + +@auth_bp.route('/authorize') +def authorize(): + from app import github + token = github.authorize_access_token() + if not token: + return redirect(url_for('main.index')) + + resp = github.get('user', token=token) + profile = resp.json() + + github_id = str(profile.get('id')) + username = profile.get('login') + avatar_url = profile.get('avatar_url') + + if not github_id: + return redirect(url_for('main.index')) + + user = User.query.filter_by(github_id=github_id).first() + if not user: + user = User( + github_id=github_id, + username=username, + avatar_url=avatar_url + ) + db.session.add(user) + else: + user.username = username + user.avatar_url = avatar_url + + db.session.commit() + + session['user_id'] = user.id + return redirect(url_for('main.profile')) + +@auth_bp.route('/logout') +def logout(): + session.pop('user_id', None) + return redirect(url_for('main.index')) diff --git a/src/routes/main_routes.py b/src/routes/main_routes.py index dec859bd..bc057688 100644 --- a/src/routes/main_routes.py +++ b/src/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, make_response +from flask import Blueprint, render_template, request, jsonify, send_from_directory, abort, make_response, redirect, url_for from utils.recommender import get_recommendations, validate_recommendation_inputs from utils.data_loader import find_project_by_id, load_all_projects, get_available_levels, get_project_stats @@ -207,8 +207,32 @@ def project_detail(project_id): project = find_project_by_id(project_id) if not project: abort(404) - return render_template("project.html", project=project, config=Config) - + + return render_template("project.html", project=project, config=Config, og_url=Config.get_base_url() + "/project/" + str(project_id)) + +@main.route("/profile") +def profile(): + from flask import session + from models import db, User + from utils.data_loader import find_project_by_id + + user_id = session.get('user_id') + if not user_id: + return redirect(url_for('auth.login')) + + user = db.session.get(User, user_id) + if not user: + session.pop('user_id', None) + return redirect(url_for('auth.login')) + + # Hydrate bookmarked projects + bookmarked_projects = [] + for pid in user.bookmarked_projects: + p = find_project_by_id(pid) + if p: + bookmarked_projects.append(p) + + return render_template("profile.html", user=user, bookmarked_projects=bookmarked_projects) @main.route("/project//code") def view_code(project_id): diff --git a/src/templates/index.html b/src/templates/index.html index 9ae9e04f..dc5107b7 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -391,6 +391,14 @@ d="M21 21l-4.35-4.35m1.85-5.15a7 7 0 11-14 0 7 7 0 0114 0z" /> + + + + + + + + + + + + + + + + +
+
+ {% if user.avatar_url %} + {{ user.username }}'s avatar + {% else %} +
+ {{ user.username[0] | upper }} +
+ {% endif %} +

{{ user.username }}

+

GitHub Developer Profile

+
+
+ + +
+

Saved Skills

+ {% if user.saved_skills %} +
+ {% for skill in user.saved_skills %} + {{ skill }} + {% endfor %} +
+ {% else %} +

You haven't saved any skills yet. Your search history will automatically populate this section over time.

+ {% endif %} + +

Bookmarked Projects

+ {% if bookmarked_projects %} +
+ {% for project in bookmarked_projects %} +
+

{{ project.title }}

+

{{ project.description | truncate(120) }}

+
+ {{ project.level }} + Time: {{ project.time }} +
+ +
+ {% endfor %} +
+ {% else %} +

You haven't bookmarked any projects yet. Go explore the catalog to find your next challenge!

+ Explore Projects + {% endif %} + +
+

Account Management

+

You are currently authenticated via GitHub OAuth.

+ Sign Out +
+
+ + + + diff --git a/tests/test_basic.py b/tests/test_basic.py index b256c61c..77219e78 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -854,6 +854,32 @@ def test_compare_api_not_found(): response = client.get("/api/compare?a=invalid&b=alsoinvalid") assert response.status_code == 404 +# ============================================================ +# Auth routes tests +# ============================================================ + +def test_login_redirect(): + """Login route should redirect to GitHub.""" + client = get_client() + response = client.get("/auth/login") + assert response.status_code == 302 + assert "github.com/login/oauth/authorize" in response.headers["Location"] + +def test_logout_redirect(): + """Logout route should redirect to homepage.""" + client = get_client() + response = client.get("/auth/logout") + assert response.status_code == 302 + assert response.headers["Location"] == "/" + +def test_profile_unauthenticated_redirects_to_login(): + """Profile route should redirect to login if unauthenticated.""" + client = get_client() + response = client.get("/profile") + assert response.status_code == 302 + assert "/auth/login" in response.headers["Location"] + + def test_sitemap_includes_compare(): """Sitemap should include the compare page.""" From b90145abcbb1d9f378a01a0d2c6b0e688b0e85c0 Mon Sep 17 00:00:00 2001 From: DESIREDDY MOHITH REDDY Date: Fri, 26 Jun 2026 17:19:50 +0530 Subject: [PATCH 3/5] Feat: Admin Dashboard for CRUD operations on projects (#1178) - Created secure /admin routes protected by is_admin check - Added is_admin column to User database model - Built dashboard.html with paginated project listings - Built form.html for creating and updating projects natively into SQLite backend - Restructured index.html navigation dynamically for admins - Added test_admin_forbidden_for_anonymous, test_admin_forbidden_for_normal_user, and test_admin_crud --- data/devpath.db | Bin 36864 -> 45056 bytes src/app.py | 3 + src/routes/admin_routes.py | 82 ++++++++++++++++++++ src/templates/admin/dashboard.html | 93 +++++++++++++++++++++++ src/templates/admin/form.html | 118 +++++++++++++++++++++++++++++ src/templates/index.html | 6 ++ tests/test_basic.py | 96 +++++++++++++++++++++++ 7 files changed, 398 insertions(+) create mode 100644 src/routes/admin_routes.py create mode 100644 src/templates/admin/dashboard.html create mode 100644 src/templates/admin/form.html diff --git a/data/devpath.db b/data/devpath.db index c1523376c2d8ff15c893df544a2d77dfa940026f..ea2ddf6439f580811623d361e35b7c6816a0036d 100644 GIT binary patch delta 599 zcmZozz|`=7X@az%AOiyfHxR=B*F+s-VL=AH-XFYt=?u)gybOH#yu5ri8w+Ri)Hmrc zv5Sj~Gq#wQBqrsgmKLWL6@v+8=O9JWw{wG054&F-XJE$UqZC3CM`N#N1Sb3IhYpdK4vzWr-z; zMe(IYIdDZrrly)83o?u26H{_C^Aw!?{e4^={ZRB3CzhqA#2060=HwJBcm?~T$|mLK zXXhpsWdoHI6y;~7CYK--CBS_U5aj9W7!;}C?HZ||k(mOvG}O;CFw|8+1MWLbE=^5l zHgQ{VMuyD1l+=pi!ko;K)cC~Gl6){f9vn{bhG2>n7y%rcv-uV}wdf=d-Wb*tmdw^BVIIj!glq0-ISU V{O8}y(!dC0CH$A)%(CDQKLGPo8&&`S diff --git a/src/app.py b/src/app.py index 867a96cc..5d03dc0c 100644 --- a/src/app.py +++ b/src/app.py @@ -50,7 +50,10 @@ # Register blueprints from routes.auth_routes import auth_bp +from routes.admin_routes import admin_bp + app.register_blueprint(auth_bp, url_prefix='/auth') +app.register_blueprint(admin_bp, url_prefix='/admin') # Register all routes defined in the main Blueprint (This handles your '/' route!) app.register_blueprint(main) diff --git a/src/routes/admin_routes.py b/src/routes/admin_routes.py new file mode 100644 index 00000000..61b2f106 --- /dev/null +++ b/src/routes/admin_routes.py @@ -0,0 +1,82 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, abort, session +from models import db, Project, User + +admin_bp = Blueprint('admin', __name__) + +@admin_bp.before_request +def check_admin(): + user_id = session.get('user_id') + if not user_id: + return redirect(url_for('auth.login')) + + user = db.session.get(User, user_id) + if not user or not user.is_admin: + abort(403) + +@admin_bp.route('/') +def dashboard(): + page = request.args.get('page', 1, type=int) + per_page = 20 + pagination = Project.query.order_by(Project.id.desc()).paginate(page=page, per_page=per_page, error_out=False) + return render_template('admin/dashboard.html', pagination=pagination) + +def _get_list_from_form(field_name): + # The form might send comma-separated strings or multiple values + val = request.form.get(field_name, '') + if not val: + return [] + # Split by comma and strip whitespace + return [item.strip() for item in val.split(',') if item.strip()] + +@admin_bp.route('/projects/new', methods=['GET', 'POST']) +def new_project(): + if request.method == 'POST': + project = Project( + title=request.form.get('title', ''), + level=request.form.get('level', ''), + interest=request.form.get('interest', ''), + time=request.form.get('time', ''), + description=request.form.get('description', ''), + skills=_get_list_from_form('skills'), + features=_get_list_from_form('features'), + tech_stack=_get_list_from_form('tech_stack'), + roadmap=_get_list_from_form('roadmap'), + resources=_get_list_from_form('resources'), + starter_code=request.form.get('starter_code', '') + ) + db.session.add(project) + db.session.commit() + return redirect(url_for('admin.dashboard')) + return render_template('admin/form.html', project=None) + +@admin_bp.route('/projects//edit', methods=['GET', 'POST']) +def edit_project(id): + project = db.session.get(Project, id) + if not project: + abort(404) + + if request.method == 'POST': + project.title = request.form.get('title', '') + project.level = request.form.get('level', '') + project.interest = request.form.get('interest', '') + project.time = request.form.get('time', '') + project.description = request.form.get('description', '') + project.skills = _get_list_from_form('skills') + project.features = _get_list_from_form('features') + project.tech_stack = _get_list_from_form('tech_stack') + project.roadmap = _get_list_from_form('roadmap') + project.resources = _get_list_from_form('resources') + project.starter_code = request.form.get('starter_code', '') + + db.session.commit() + return redirect(url_for('admin.dashboard')) + + return render_template('admin/form.html', project=project) + +@admin_bp.route('/projects//delete', methods=['POST']) +def delete_project(id): + project = db.session.get(Project, id) + if project: + db.session.delete(project) + db.session.commit() + return redirect(url_for('admin.dashboard')) diff --git a/src/templates/admin/dashboard.html b/src/templates/admin/dashboard.html new file mode 100644 index 00000000..38a450d4 --- /dev/null +++ b/src/templates/admin/dashboard.html @@ -0,0 +1,93 @@ + + + + + + Admin Dashboard | DevPath + + + +
+
+
+

Projects Dashboard

+

Manage the DevPath projects database.

+
+ +
+ + + + + + + + + + + + + + {% for project in pagination.items %} + + + + + + + + + {% endfor %} + +
IDTitleLevelInterestTimeActions
#{{ project.id }}{{ project.title }}{{ project.level }}{{ project.interest }}{{ project.time }} + View + Edit +
+ +
+
+ + +
+ + diff --git a/src/templates/admin/form.html b/src/templates/admin/form.html new file mode 100644 index 00000000..1dca3b24 --- /dev/null +++ b/src/templates/admin/form.html @@ -0,0 +1,118 @@ + + + + + + {% if project %}Edit Project{% else %}New Project{% endif %} | DevPath Admin + + + +
+
+

{% if project %}Edit Project #{{ project.id }}{% else %}Create New Project{% endif %}

+
+ +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ + +
+ +
+ + + Used for the recommendation engine algorithm +
+ +
+ + +
+ +
+ + + What will the user build? Each item becomes a bullet point. +
+ +
+ + + Step-by-step instructions. Each item becomes a numbered list item. +
+ +
+ + + Helpful links for the user to complete the project. +
+ +
+ + + Leave blank if there is no starter code. +
+ +
+ + Cancel +
+
+
+ + diff --git a/src/templates/index.html b/src/templates/index.html index dc5107b7..a7610854 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -394,6 +394,9 @@