from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort, jsonify # from .models import User # from . import db from .email_utils import send_verification_email from .code_generator import generate_ranker_code from .utils import rank_resumes_by_similarity from werkzeug.utils import secure_filename from .ranker import rank_resumes_by_semantic_similarity import traceback from werkzeug.security import generate_password_hash, check_password_hash from .supabase_client import get_user_by_email, create_user import uuid from .supabase_config import SUPABASE_URL, SUPABASE_API_KEY import secrets import os import requests api = Blueprint('api', __name__) SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_API_KEY = os.getenv("SUPABASE_API_KEY") HEADERS = { "apikey": SUPABASE_API_KEY, "Authorization": f"Bearer {SUPABASE_API_KEY}", "Content-Type": "application/json" } MOCK_USER = { 'username': 'Alice', 'api_key': 'rk_DEMO_KEY_1a2b3c4d5e6f7g8h', } MOCK_ANALYTICS = { 'average_score': '85%', 'top_resume': 'Software Engineer - Jane Doe.pdf', 'last_upload_date': '2025-07-07', } MOCK_ALL_UPLOADS = [ # More data for the full rankings page {'id': 1, 'name': 'John_Doe_CV.pdf', 'score': '92%', 'uploaded_date': '2025-07-06', 'job_description_summary': 'Software Engineer at Google...'}, {'id': 2, 'name': 'Software_Dev_Resume.docx', 'score': '88%', 'uploaded_date': '2025-07-05', 'job_description_summary': 'Backend Developer for Fintech...'}, {'id': 3, 'name': 'Project_Manager_Exp.pdf', 'score': '75%', 'uploaded_date': '2025-07-04', 'job_description_summary': 'Lead project teams for Agile...'}, {'id': 4, 'name': 'Marketing_Specialist.pdf', 'score': '65%', 'uploaded_date': '2025-07-03', 'job_description_summary': 'Digital Marketing Expert...'}, {'id': 5, 'name': 'Data_Analyst_Resume.docx', 'score': '80%', 'uploaded_date': '2025-07-02', 'job_description_summary': 'Analyze large datasets...'}, ] auth = Blueprint('auth', __name__) @auth.route('/') def landing(): if 'user_id' in session: return redirect(url_for('auth.dashboard')) return render_template('landing.html') # @api.route('/rank-resumes', methods=['GET', 'POST']) # def rank_resumes(): # if request.method == 'GET': # return render_template("upload_rank_form.html") # Optional: A form to test this route manually # # 1. Extract and validate API key # auth_header = request.headers.get('Authorization') # if not auth_header or not auth_header.startswith("Bearer "): # return render_template("error.html", error="Missing or invalid Authorization header"), 401 # api_key = auth_header.split(" ")[1] # # 2. Query Supabase to validate API key # response = requests.get( # f"{SUPABASE_URL}/rest/v1/users?api_key=eq.{api_key}", # headers=HEADERS # ) # if response.status_code != 200 or not response.json(): # return render_template("error.html", error="Invalid API key"), 403 # # 3. Parse form data # job_description = request.form.get('job_description') # resume_files = request.files.getlist('resumes') # if not job_description: # return render_template("error.html", error="Missing job description"), 400 # if not resume_files: # return render_template("error.html", error="No resumes uploaded"), 400 # # 4. Rank resumes # try: # results = rank_resumes_by_semantic_similarity(resume_files, job_description) # return render_template( # "resume_rankings.html", # job_description=job_description, # ranked_resumes=results # ) # except Exception as e: # return render_template("error.html", error=f"Error processing resumes: {str(e)}"), 500 @auth.route('/rank-resumes', methods=['GET', 'POST']) def rank_resumes(): api_key = request.form.get('api_key') job_description = request.form.get('job_description') resume_files = request.files.getlist('resumes') if not api_key or not job_description or not resume_files: flash("All fields are required.") return redirect(url_for('auth.rank_resumes')) # Validate API key with Supabase response = requests.get( f"{SUPABASE_URL}/rest/v1/users?api_key=eq.{api_key}", headers=HEADERS ) if response.status_code != 200 or not response.json(): flash("Invalid API key.") return redirect(url_for('auth.rank_resumes')) try: rankings = rank_resumes_by_semantic_similarity(resume_files, job_description) except Exception as e: flash(f"Ranking error: {str(e)}") return redirect(url_for('auth.rank_resumes')) return render_template('resume_rankings.html', rankings=rankings, job_description=job_description) @auth.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') email = request.form.get('email') password = request.form.get('password') if not username or not email or not password: flash('Please fill all fields.', 'error') return render_template("register.html") # Check if email already exists check_response = requests.get( f"{SUPABASE_URL}/rest/v1/users?email=eq.{email}", headers=HEADERS ) if check_response.status_code == 200 and check_response.json(): flash('Email already registered. Please log in.', 'warning') return redirect(url_for('auth.login')) hashed_password = generate_password_hash(password) token = str(uuid.uuid4()) payload = { "username": username, "email": email, "password": hashed_password, "email_token": token, "is_verified": False } response = requests.post(f"{SUPABASE_URL}/rest/v1/users", json=payload, headers=HEADERS) if response.status_code == 201: try: send_verification_email(email, username, token) flash('Account created. Check your email to verify.', 'success') except Exception as e: print("Email sending failed:", e) flash('Registered, but email failed to send.', 'warning') return redirect(url_for('auth.login')) else: print("Supabase error:", response.text) flash('Failed to register. Try again.', 'error') return render_template("register.html") @auth.route('/login', methods=['GET', 'POST']) def login(): if 'user_id' in session: return redirect(url_for('auth.dashboard')) if request.method == 'POST': email = request.form['email'] password = request.form['password'] # Query Supabase for user by email headers = { "apikey": SUPABASE_API_KEY, "Authorization": f"Bearer {SUPABASE_API_KEY}" } response = requests.get( f"{SUPABASE_URL}/rest/v1/users?email=eq.{email}", headers=headers ) if response.status_code != 200 or not response.json(): flash('Invalid email or password.') return redirect(url_for('auth.login')) user_data = response.json()[0] # Check password if not check_password_hash(user_data['password'], password): flash('Invalid email or password.') return redirect(url_for('auth.login')) # Check if user is verified if not user_data.get('is_verified', False): flash('Please verify your email before logging in.') return redirect(url_for('auth.login')) # Login successful session['user_id'] = user_data['id'] session['username'] = user_data['username'] flash('Logged in successfully.') return redirect(url_for('auth.dashboard')) return render_template('login.html') @auth.route('/resend-verification', methods=['GET', 'POST']) def resend_verification(): if request.method == 'POST': try: email = request.form['email'] # Fetch user by email headers = { "apikey": SUPABASE_API_KEY, "Authorization": f"Bearer {SUPABASE_API_KEY}" } res = requests.get( f"{SUPABASE_URL}/rest/v1/users?email=eq.{email}&select=*", headers=headers ) if res.status_code != 200 or not res.json(): flash('No account found with that email.') return redirect(url_for('auth.login')) user = res.json()[0] if user.get("is_verified") == True: flash('Your email is already verified. You can log in.') return redirect(url_for('auth.login')) # Generate new token new_token = str(uuid.uuid4()) # Update `email_token` in Supabase patch_res = requests.patch( f"{SUPABASE_URL}/rest/v1/users?email=eq.{email}", headers={**headers, "Content-Type": "application/json"}, json={"email_token": new_token} ) if patch_res.status_code != 204: print("Failed to update token:", patch_res.text) return jsonify({"error": "Failed to update verification token."}), 500 # Send the verification email result = send_verification_email(user['email'], user['username'], new_token) if result is True: flash('Verification email resent. Please check your inbox.') else: print("Email error from resend:", result) return jsonify(result), 500 return redirect(url_for('auth.login')) except Exception as e: print("Resend error:", e) traceback.print_exc() return jsonify({"error": str(e)}), 500 return render_template('resend_verification.html') @auth.route('/dashboard') def dashboard(): if 'user_id' not in session: flash('Please log in to access the dashboard.') return redirect(url_for('auth.login')) user_id = session['user_id'] # Fetch user info user_res = requests.get( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=*", headers=HEADERS ) if user_res.status_code != 200 or not user_res.json(): flash('User not found.') return redirect(url_for('auth.login')) user = user_res.json()[0] # Fetch user's resume uploads uploads_res = requests.get( f"{SUPABASE_URL}/rest/v1/resume-api?user_id=eq.{user_id}&select=*", headers=HEADERS ) analytics = { 'average_score': None, 'top_resume': None, 'last_upload_date': None } if uploads_res.status_code == 200: uploads = uploads_res.json() if uploads: scores = [u['score'] for u in uploads if u.get('score') is not None] if scores: analytics['average_score'] = round(sum(scores) / len(scores), 2) top = max(uploads, key=lambda x: x.get('score', 0)) analytics['top_resume'] = top.get('resume_name', 'N/A') last_upload = max(uploads, key=lambda x: x.get('upload_date', '')) analytics['last_upload_date'] = last_upload.get('upload_date', '')[:16] # YYYY-MM-DD HH:MM return render_template('dashboard.html', user=user, analytics=analytics) @auth.route('/generate-api', methods=['GET', 'POST']) def generate_api(): if 'user_id' not in session: flash('Please log in to access the API generator.') return redirect(url_for('auth.login')) if request.method == 'POST': flask_code, html_code = generate_ranker_code() return render_template( 'generated_code.html', flask_code=flask_code, html_code=html_code, title="Resume Ranker API Code" ) return render_template('generate_api.html', title="Generate Resume Ranker API") def generate_api_key(): return f"rk_live_{secrets.token_urlsafe(24)}" @auth.route('/generate-initial-api-key', methods=['POST']) def generate_initial_api_key(): if 'user_id' not in session: flash('Please log in.') return redirect(url_for('auth.login')) user_id = session['user_id'] new_key = generate_api_key() res = requests.patch( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}", headers={**HEADERS, "Content-Type": "application/json"}, json={"api_key": new_key} ) if res.status_code == 200 or res.status_code == 204: flash("API key generated successfully.") else: flash("Failed to generate API key. Please try again.") return redirect(url_for('auth.api_keys')) @auth.route('/regenerate-api-key', methods=['POST']) def regenerate_api_key(): if 'user_id' not in session: flash('Please log in.') return redirect(url_for('auth.login')) user_id = session['user_id'] new_key = generate_api_key() res = requests.patch( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}", headers={**HEADERS, "Content-Type": "application/json"}, json={"api_key": new_key} ) if res.status_code == 200 or res.status_code == 204: flash("API key regenerated successfully.") else: flash("Failed to regenerate API key. Please try again.") return redirect(url_for('auth.api_keys')) @auth.route('/logout') def logout(): session.clear() flash('You have been logged out.') return redirect(url_for('auth.login')) @auth.route('/api_keys', methods=['GET']) def api_keys(): if 'user_id' not in session: flash('Please log in to view your API key.') return redirect(url_for('auth.login')) user_id = session['user_id'] res = requests.get( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=*", headers=HEADERS ) if res.status_code != 200 or not res.json(): flash('Failed to fetch your account.') return redirect(url_for('auth.dashboard')) user = res.json()[0] return render_template('api_keys.html', user=user, title="My API Keys") @auth.route('/verify/') def verify_email(token): res = requests.get( f"{SUPABASE_URL}/rest/v1/users?email_token=eq.{token}&select=*", headers=HEADERS ) if res.status_code != 200 or not res.json(): flash('Invalid or expired verification link.') return redirect(url_for('auth.register')) user = res.json()[0] email = user["email"] patch = requests.patch( f"{SUPABASE_URL}/rest/v1/users?email=eq.{email}", headers=HEADERS, json={"is_verified": True, "email_token": None} ) if patch.status_code == 204: flash('Email verified! You can now log in.') return redirect(url_for('auth.login')) else: flash('Verification failed.') return redirect(url_for('auth.register')) @auth.route('/api-docs') def api_docs(): if 'user_id' not in session: flash('Please log in to view the API documentation.') return redirect(url_for('auth.login')) # Optionally fetch user info (for personalization in template) user_id = session['user_id'] user_res = requests.get( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=*", headers=HEADERS ) user = user_res.json()[0] if user_res.status_code == 200 and user_res.json() else {} return render_template('api_docs.html', title="API Documentation", user=user) @auth.route('/settings') def settings(): if 'user_id' not in session: flash('Please log in to access settings.') return redirect(url_for('auth.login')) user_id = session['user_id'] user_res = requests.get( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=*", headers=HEADERS ) user = user_res.json()[0] if user_res.status_code == 200 and user_res.json() else {} return render_template('settings.html', title="User Settings", user=user) # @auth.route('/docs') # def docs(): # return render_template('docs.html', response=None) # allowed_extensions = {'pdf', 'docx'} # def allowed_file(filename): # return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions # @auth.route('/try-api', methods=['GET', 'POST']) # def try_api(): # api_key = request.form.get('api_key') # resumes = request.files.getlist('resumes') # job_description = request.form.get('job_description') # # Validate form fields # if not api_key: # flash("API key is required.") # return redirect(url_for('auth.docs')) # if not resumes: # flash("Please upload at least one resume.") # return redirect(url_for('auth.docs')) # if not job_description: # flash("Job description is required.") # return redirect(url_for('auth.docs')) # # Validate API key from Supabase # res = requests.get( # f"{SUPABASE_URL}/rest/v1/users?api_key=eq.{api_key}&select=id", # headers=HEADERS # ) # if res.status_code != 200 or not res.json(): # flash("Invalid API key.") # return redirect(url_for('auth.docs')) # # Validate file types # for resume in resumes: # if not allowed_file(resume.filename): # flash(f"Invalid file type: {resume.filename}. Only PDF or DOCX allowed.") # return redirect(url_for('auth.docs')) # resume.filename = secure_filename(resume.filename) # # Run actual ranking logic # try: # ranked = rank_resumes_by_semantic_similarity(resumes, job_description) # except Exception as e: # flash(f"Error processing resumes: {str(e)}") # return redirect(url_for('auth.docs')) # # Render results # return render_template('docs.html', response={ # "job_description": job_description, # "ranked_resumes": ranked # List of dicts with filename and score # }) @auth.route('/try-api', methods=['GET', 'POST']) def try_api(): if 'user_id' not in session: flash('Please log in to test the API.') return redirect(url_for('auth.login')) user_id = session['user_id'] # Fetch user info user_res = requests.get( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user_id}&select=*", headers=HEADERS ) if user_res.status_code != 200 or not user_res.json(): flash('User not found.') return redirect(url_for('auth.login')) user = user_res.json()[0] if request.method == 'GET': return render_template('try_api.html', title="Try the ResumeRanker API", user=user) # POST logic api_key = request.form.get('api_key') resumes = request.files.getlist('resumes') job_description = request.form.get('job_description') if not api_key: flash("API key is required.") return redirect(url_for('auth.try_api')) if not resumes: flash("Please upload at least one resume.") return redirect(url_for('auth.try_api')) if not job_description: flash("Job description is required.") return redirect(url_for('auth.try_api')) res = requests.get( f"{SUPABASE_URL}/rest/v1/users?api_key=eq.{api_key}&select=id", headers=HEADERS ) if res.status_code != 200 or not res.json(): flash("Invalid API key.") return redirect(url_for('auth.try_api')) for resume in resumes: if not allowed_file(resume.filename): flash(f"Invalid file type: {resume.filename}. Only PDF or DOCX allowed.") return redirect(url_for('auth.try_api')) resume.filename = secure_filename(resume.filename) try: ranked = rank_resumes_by_semantic_similarity(resumes, job_description) except Exception as e: flash(f"Error processing resumes: {str(e)}") return redirect(url_for('auth.try_api')) return render_template('try_api.html', user=user, response={ "job_description": job_description, "ranked_resumes": ranked }) @auth.route('/ping-db') def ping_db(): try: res = requests.get( f"{SUPABASE_URL}/rest/v1/users?select=id", headers=HEADERS ) if res.status_code == 200: count = len(res.json()) return f" Connected to Supabase. Users count: {count}" else: return f" Failed to fetch users: {res.text}" except Exception as e: return f" DB Error: {e}" # @auth.route('/settings') # def settings(): # user, redirect_response = get_current_user() # if redirect_response: # return redirect_response # return render_template('settings.html', user=user) @auth.route('/update_profile', methods=['POST']) def update_profile(): user, redirect_response = get_current_user() if redirect_response: return redirect_response new_email = request.form.get('email') # username = request.form.get('username') # Username is read-only, not updated here if not new_email: flash('Email cannot be empty.', 'error') return redirect(url_for('auth.settings')) # Update email in Supabase. Assuming 'users' table or Supabase Auth update. # For Supabase Auth, you'd use the client library or the /auth/v1/users endpoint # For a custom 'users' table, it would be a PATCH request: update_payload = {"email": new_email} update_res = requests.patch( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user['id']}", headers=HEADERS, json=update_payload ) if update_res.status_code == 200 or update_res.status_code == 204: # 200 OK or 204 No Content flash('Profile updated successfully!', 'success') else: flash(f'Failed to update profile. Error: {update_res.text}', 'error') return redirect(url_for('auth.settings')) @auth.route('/change_password', methods=['POST']) def change_password(): user, redirect_response = get_current_user() if redirect_response: return redirect_response current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_new_password = request.form.get('confirm_new_password') if not current_password or not new_password or not confirm_new_password: flash('All password fields are required.', 'error') return redirect(url_for('auth.settings')) if new_password != confirm_new_password: flash('New password and confirmation do not match.', 'error') return redirect(url_for('auth.settings')) # In a real app, you would verify current_password first: # 1. Fetch user from Supabase Auth based on their email or ID. # 2. Use `check_password_hash(user.hashed_password, current_password)` # If you're using Supabase's built-in auth, you'd typically re-authenticate # or use their client's `updateUser` function which handles password changes. # Direct patching to a 'users' table password field is generally insecure # unless you are doing the hashing yourself. # Example using a mock check for demonstration if current_password != 'TestPass123': # Replace with actual password verification flash('Incorrect current password.', 'error') return redirect(url_for('auth.settings')) # Update password in Supabase Auth (conceptual) # This typically involves a call to Supabase's auth client or API endpoint for password change. # E.g., `supabase.auth.updateUser({ password: new_password })` # For direct database table if not using Supabase Auth for passwords: hashed_new_password = generate_password_hash(new_password) update_payload = {"password_hash": hashed_new_password} # Assuming a column for hashed password update_res = requests.patch( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user['id']}", headers=HEADERS, json=update_payload ) if update_res.status_code == 200 or update_res.status_code == 204: flash('Password changed successfully!', 'success') else: flash(f'Failed to change password. Error: {update_res.text}', 'error') flash('Password change simulated successfully!', 'success') # Placeholder return redirect(url_for('auth.settings')) @auth.route('/delete_account', methods=['POST']) def delete_account(): user, redirect_response = get_current_user() if redirect_response: return redirect_response # In a real app, confirm with a password or MFA # Then delete the user from Supabase Auth and any related tables (via RLS or triggers) # Example for deleting from a custom 'users' table: delete_res = requests.delete( f"{SUPABASE_URL}/rest/v1/users?id=eq.{user['id']}", headers=HEADERS ) if delete_res.status_code == 204: # 204 No Content for successful delete session.pop('user_id', None) # Clear session after account deletion flash('Your account has been successfully deleted.', 'success') return redirect(url_for('auth.login')) else: flash(f'Failed to delete account. Error: {delete_res.text}', 'error') return redirect(url_for('auth.settings'))