webcodelab's picture
Update app/routes.py
a719504 verified
Raw
History Blame Contribute Delete
25.4 kB
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/<token>')
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'))