webcodelab commited on
Commit
4b63522
·
verified ·
1 Parent(s): 7c0b54b

initial commit

Browse files
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Webcodelab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,12 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: AI Resume Ranker API
3
- emoji: 📊
4
- colorFrom: yellow
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Resume
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧠 AI Resume Ranker API
2
+
3
+ An intelligent, production-ready Resume Ranking API for developers, recruiters, and HR tech platforms.
4
+ Inspired by Stripe & Paystack API experiences — complete with authentication, dashboard, and live testing interface.
5
+
6
+ Built with **Flask + Sentence Transformers + PDF Parsing + Semantic AI**, this API helps you automatically score and rank resumes against any job description using powerful language understanding models.
7
+
8
+ ---
9
+
10
+ ## 🚀 Features
11
+
12
+ - ✅ **API Key Authentication** with secure Bearer token format
13
+ - 🧾 **Semantic Resume Ranking** using transformer embeddings (not just keyword matching)
14
+ - 📤 Supports **batch PDF uploads**
15
+ - 📬 **JSON API responses** with ranked relevance scores
16
+ - 🧪 **Live Testing Interface** on the dashboard
17
+ - 🔐 **Secure User System** with login, signup, hashed passwords
18
+ - 🔁 **API Key Regeneration** from the dashboard
19
+ - 📚 Beautiful **API Documentation Page** with copyable code samples
20
+ - 🧰 **Try It Now**: Upload resumes + job text and see live scoring
21
+ - 📈 Ready for usage tracking, analytics, or rate limiting
22
+ - 💼 Built for scaling into a full SaaS product
23
+
24
+ ---
25
+
26
+ ## 🌐 Live Demo
27
+
28
+ > Coming soon: hosted on Render or Railway (e.g., [https://resume-ranker.example.com](https://resume-ranker.example.com))
29
+
30
  ---
31
+
32
+ ## 📦 API Overview
33
+
34
+ ### 🔗 Endpoint
35
+
36
+ ```
37
+
38
+ POST /api/rank-resumes
39
+
40
+ ```
41
+
42
+ ### 🔐 Authentication
43
+
44
+ Send your API key in the request header:
45
+
46
+ ```
47
+
48
+ Authorization: Bearer amn=your\_api\_key\_here
49
+
50
+ ````
51
+
52
+ ### 📤 Request Parameters
53
+
54
+ | Name | Type | Required | Description |
55
+ |-----------------|------------------|----------|--------------------------------------|
56
+ | resumes | `file[] (PDF)` | ✅ Yes | One or more PDF resumes |
57
+ | job_description | `string` | ✅ Yes | Job description text to compare with |
58
+
59
+ ---
60
+
61
+ ## 📥 Example Usage
62
+
63
+ ### 🧪 Try It via cURL
64
+
65
+ ```bash
66
+ curl -X POST https://yourdomain.com/api/rank-resumes \
67
+ -H "Authorization: Bearer amn=sk_live_abc123xyz" \
68
+ -F "resumes=@resume1.pdf" \
69
+ -F "resumes=@resume2.pdf" \
70
+ -F "job_description=We are hiring a backend Django developer..."
71
+ ````
72
+
73
+ ### 📦 Sample Response
74
+
75
+ ```json
76
+ {
77
+ "results": [
78
+ {
79
+ "filename": "resume1.pdf",
80
+ "score": 0.8745
81
+ },
82
+ {
83
+ "filename": "resume2.pdf",
84
+ "score": 0.6721
85
+ }
86
+ ],
87
+ "count": 2,
88
+ "requested_by": "user@example.com"
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 🛠 Tech Stack
95
+
96
+ * **Backend:** Flask, Blueprints, Jinja2
97
+ * **AI:** SentenceTransformers (`all-MiniLM-L6-v2`)
98
+ * **PDF Parsing:** PyMuPDF
99
+ * **Auth:** Flask sessions + hashed passwords
100
+ * **Database:** SQLite (dev) / PostgreSQL-ready
101
+ * **Frontend:** Bootstrap 5 + custom Jinja templates
102
+ * **Hosting:** Render / Railway / PythonAnywhere
103
+ * **Docs UI:** Fully embedded HTML + live form
104
+
105
+ ---
106
+
107
+ ## 🧪 Try It Now (via Dashboard)
108
+
109
+ * Register/Login
110
+ * View and copy your API Key
111
+ * Paste a job description
112
+ * Upload 1–5 resumes (PDF)
113
+ * Get AI-scored ranking results instantly
114
+
115
  ---
116
 
117
+ ## 🧑‍💻 Developer Setup
118
+
119
+ ```bash
120
+ git clone https://github.com/yourusername/resume-ranker-api.git
121
+ cd resume-ranker-api
122
+ python -m venv venv
123
+ source venv/bin/activate # or venv\Scripts\activate on Windows
124
+ pip install -r requirements.txt
125
+ flask run
126
+ ```
127
+
128
+ Perfect — thanks for the clarification.
129
+
130
+ You're right to **remove the real email password** before pushing to GitHub — sensitive credentials should never be committed. Instead, include **placeholder values** in the `.env` section of your `README.md`.
131
+
132
+ ---
133
+
134
+ ## ✅ Updated `.env` Example for README
135
+
136
+ Here's the correct `.env` block to include in the `README.md`:
137
+
138
+ ```ini
139
+ FLASK_ENV=development
140
+ SECRET_KEY=your_secret_key_here
141
+ DATABASE_URL=sqlite:///db.sqlite3
142
+
143
+ # ✅ Email credentials for verification system
144
+ EMAIL_USER=your_email@gmail.com
145
+ EMAIL_PASS=your_app_password_here
146
+ ```
147
+
148
+ > 📌 **Note:** Use an **App Password** (not your actual Gmail password) if you're using Gmail SMTP. App passwords are safer and Gmail-compliant.
149
+
150
+ ---
151
+
152
+ ## 🔒 Additional Security Tip
153
+
154
+ To prevent accidental exposure:
155
+
156
+ * Add `.env` to your `.gitignore`
157
+ * Use environment variables in production (Render, Railway, Fly.io all support this)
158
+
159
+ **`.gitignore` entry:**
160
+
161
+ .env
162
+ ```
163
+
164
+ ---
165
+
166
+ ## ✅ Bonus (Optional): Mention in README
167
+
168
+ You can also add a small note under **Developer Setup** in your README:
169
+
170
+ > 📧 **Note:** To enable email verification links, set `EMAIL_USER` and `EMAIL_PASS` in your `.env`. We recommend using an App Password with Gmail or a transactional email provider like Mailgun or SendGrid.
171
+
172
+
173
+ ## 💡 Future Features
174
+
175
+ * ✅ API usage tracking
176
+ * ⏳ Rate limiting (per API key)
177
+ * 📊 Dashboard analytics
178
+ * 🧠 Resume summarization API
179
+ * 📁 CSV/JSON result export
180
+ * 🧩 SDKs for Python & JS
181
+
182
+ ---
183
+
184
+ ## 🤝 Contributing
185
+
186
+ Pull requests are welcome! Let's build the future of AI recruiting tools together.
187
+
188
+ ---
189
+
190
+ ## 📜 License
191
+
192
+ MIT License
193
+
194
+ ---
195
+
196
+ ## 📬 Contact
197
+
198
+ Made with ❤️ by [Muhammad Aminu Umar](mailto:webcodelabb@gmail.com)
199
+ 🔗 [LinkedIn](https://linkedin.com/in/webcodelab) | [GitHub](https://github.com/webcodelabb)
200
+
app/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_sqlalchemy import SQLAlchemy
3
+ import os
4
+
5
+ db = SQLAlchemy()
6
+
7
+ def create_app():
8
+ app = Flask(__name__)
9
+ app.config['SECRET_KEY'] = 'your-secret-key'
10
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
11
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
12
+
13
+ db.init_app(app)
14
+
15
+ with app.app_context():
16
+ from .models import User
17
+ db.create_all()
18
+
19
+ # ✅ Import and register the blueprint
20
+ from .routes import auth
21
+ app.register_blueprint(auth)
22
+
23
+ return app
app/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (1.14 kB). View file
 
app/__pycache__/code_generator.cpython-313.pyc ADDED
Binary file (1.09 kB). View file
 
app/__pycache__/email_utils.cpython-313.pyc ADDED
Binary file (1.74 kB). View file
 
app/__pycache__/models.cpython-313.pyc ADDED
Binary file (2.85 kB). View file
 
app/__pycache__/ranker.cpython-313.pyc ADDED
Binary file (1.44 kB). View file
 
app/__pycache__/routes.cpython-313.pyc ADDED
Binary file (11 kB). View file
 
app/__pycache__/utils.cpython-313.pyc ADDED
Binary file (3.21 kB). View file
 
app/code_generator.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def generate_ranker_code():
2
+ flask_code = '''@app.route('/rank-resumes', methods=['POST'])
3
+ def rank_resumes():
4
+ resumes = request.files.getlist('resumes')
5
+ job_description = request.form.get('job_description')
6
+
7
+ # Placeholder logic — replace with your ranking algorithm
8
+ ranked = [resume.filename for resume in resumes]
9
+ return jsonify({
10
+ "job_description": job_description,
11
+ "ranked_resumes": ranked
12
+ })'''
13
+
14
+ html_code = '''<form method="POST" action="/rank-resumes" enctype="multipart/form-data">
15
+ <label>Upload Resumes:</label><br>
16
+ <input type="file" name="resumes" multiple required><br><br>
17
+
18
+ <label>Paste Job Description:</label><br>
19
+ <textarea name="job_description" rows="6" cols="50" required></textarea><br><br>
20
+
21
+ <button type="submit">Rank Resumes</button>
22
+ </form>'''
23
+
24
+ return flask_code, html_code
app/decorators.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/auth.py
2
+ from functools import wraps
3
+ from flask import request, jsonify
4
+ from .models import User # adjust import to your structure
5
+
6
+ def require_api_key(f):
7
+ @wraps(f)
8
+ def decorated_function(*args, **kwargs):
9
+ auth_header = request.headers.get('Authorization')
10
+
11
+ if not auth_header or not auth_header.startswith('Bearer '):
12
+ return jsonify({"error": "Missing or invalid Authorization header."}), 401
13
+
14
+ token = auth_header.split("Bearer ")[1].strip()
15
+
16
+ user = User.query.filter_by(api_key=token).first()
17
+ if not user:
18
+ return jsonify({"error": "Invalid or revoked API key."}), 403
19
+
20
+ request.user = user # attach user to request for downstream use
21
+ return f(*args, **kwargs)
22
+ return decorated_function
app/email_utils.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import smtplib
2
+ from email.mime.text import MIMEText
3
+ from email.mime.multipart import MIMEMultipart
4
+ import os
5
+
6
+ def send_verification_email(to_email, username, token):
7
+ from_email = os.getenv("EMAIL_USER")
8
+ app_password = os.getenv("EMAIL_PASS")
9
+
10
+ subject = "Verify your account"
11
+ verification_link = f"http://127.0.0.1:5000/verify/{token}"
12
+ body = f"""
13
+ Hi {username},
14
+
15
+ Thanks for registering! Please verify your email by clicking the link below:
16
+
17
+ {verification_link}
18
+
19
+ If you didn't sign up, you can ignore this email.
20
+ """
21
+
22
+ msg = MIMEMultipart()
23
+ msg['From'] = from_email
24
+ msg['To'] = to_email
25
+ msg['Subject'] = subject
26
+
27
+ msg.attach(MIMEText(body, 'plain'))
28
+
29
+ try:
30
+ with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
31
+ server.login(from_email, app_password)
32
+ server.send_message(msg)
33
+ print(f"Verification email sent to {to_email}")
34
+ return True
35
+ except Exception as e:
36
+ print(f"Failed to send email: {e}")
37
+ return False
app/models.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import db
2
+ from werkzeug.security import generate_password_hash, check_password_hash
3
+ import uuid
4
+ import secrets
5
+
6
+ class User(db.Model):
7
+ id = db.Column(db.Integer, primary_key=True)
8
+ username = db.Column(db.String(150), unique=True, nullable=False)
9
+ email = db.Column(db.String(150), unique=True, nullable=False)
10
+ password_hash = db.Column(db.String(256), nullable=False)
11
+
12
+ is_verified = db.Column(db.Boolean, default=False)
13
+ verification_token = db.Column(db.String(64), unique=True, nullable=True, default=lambda: str(uuid.uuid4()))
14
+
15
+ api_key = db.Column(db.String(64), unique=True, nullable=True, default=lambda: secrets.token_hex(32))
16
+
17
+ def generate_api_key(self):
18
+ raw_key = secrets.token_hex(32)
19
+ self.api_key = f"amn={raw_key}"
20
+ db.session.commit()
21
+
22
+ def generate_verification_token(self):
23
+ self.verification_token = str(uuid.uuid4())
24
+ db.session.commit()
25
+
26
+ def set_password(self, password):
27
+ self.password_hash = generate_password_hash(password)
28
+
29
+ def check_password(self, password):
30
+ return check_password_hash(self.password_hash, password)
app/ranker.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/ranker.py
2
+ from sentence_transformers import SentenceTransformer, util
3
+ from .utils import extract_text_from_pdf
4
+
5
+ model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
6
+ # You can load this once globally
7
+
8
+ def rank_resumes_by_semantic_similarity(resume_files, job_description):
9
+ resume_texts = [extract_text_from_pdf(f) for f in resume_files]
10
+ job_embedding = model.encode(job_description, convert_to_tensor=True)
11
+
12
+ results = []
13
+ for file, resume_text in zip(resume_files, resume_texts):
14
+ resume_embedding = model.encode(resume_text, convert_to_tensor=True)
15
+ similarity = util.cos_sim(job_embedding, resume_embedding).item()
16
+ results.append({
17
+ "filename": file.filename,
18
+ "score": round(similarity, 4)
19
+ })
20
+
21
+ results.sort(key=lambda x: x['score'], reverse=True)
22
+ return results
app/routes.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort, jsonify
2
+ from .models import User
3
+ from . import db
4
+ from .email_utils import send_verification_email
5
+ from .code_generator import generate_ranker_code
6
+ from .utils import rank_resumes_by_similarity
7
+ import requests
8
+ from werkzeug.utils import secure_filename
9
+ from .ranker import rank_resumes_by_semantic_similarity
10
+
11
+ auth = Blueprint('auth', __name__)
12
+
13
+ @auth.route('/')
14
+ def home():
15
+ return jsonify({"message": "Flask API is live on Hugging Face!"})
16
+
17
+ @auth.route('/embed', methods=['POST'])
18
+ def embed():
19
+ data = request.json
20
+ text = data.get("text", "")
21
+ return jsonify({"embedding": [0.1, 0.2, 0.3]}) # Dummy
22
+
23
+ @auth.route('/rank-resumes', methods=['POST'])
24
+ def rank_resumes():
25
+ auth_header = request.headers.get('Authorization')
26
+ if not auth_header or not auth_header.startswith('Bearer '):
27
+ return jsonify({"error": "Missing or invalid Authorization header"}), 401
28
+
29
+ api_key = auth_header.split(" ")[1]
30
+ user = User.query.filter_by(api_key=api_key).first()
31
+ if not user:
32
+ return jsonify({"error": "Invalid API key"}), 403
33
+
34
+ resumes = request.files.getlist('resumes')
35
+ job_description = request.form.get('job_description')
36
+
37
+ if not resumes or not job_description:
38
+ return jsonify({"error": "Missing resumes or job description"}), 400
39
+
40
+ ranked = rank_resumes_by_semantic_similarity(resumes, job_description)
41
+
42
+ return jsonify({
43
+ "job_description": job_description,
44
+ "ranked_resumes": ranked
45
+ })
46
+
47
+
48
+
49
+ @auth.route('/register', methods=['GET', 'POST'])
50
+ def register():
51
+ if request.method == 'POST':
52
+ username = request.form['username']
53
+ email = request.form['email']
54
+ password = request.form['password']
55
+
56
+ if User.query.filter_by(email=email).first():
57
+ flash('Email already registered.')
58
+ return redirect(url_for('auth.register'))
59
+
60
+ new_user = User(username=username, email=email)
61
+ new_user.set_password(password)
62
+ new_user.generate_verification_token()
63
+ db.session.add(new_user)
64
+ db.session.commit()
65
+
66
+ # Send verification email
67
+ if send_verification_email(email, username, new_user.verification_token):
68
+ flash('Registration successful! Please check your email to verify your account.')
69
+ else:
70
+ flash('Registration succeeded, but failed to send verification email.')
71
+ new_user.generate_api_key()
72
+ db.session.commit()
73
+ return redirect(url_for('auth.login'))
74
+
75
+ return render_template('register.html')
76
+
77
+ @auth.route('/login', methods=['GET', 'POST'])
78
+ def login():
79
+ if request.method == 'POST':
80
+ email = request.form['email']
81
+ password = request.form['password']
82
+ user = User.query.filter_by(email=email).first()
83
+
84
+ if user and user.check_password(password):
85
+ if not user.is_verified:
86
+ flash('Please verify your email before logging in.')
87
+ return redirect(url_for('auth.login'))
88
+
89
+ session['user_id'] = user.id
90
+ session['username'] = user.username
91
+ flash('Logged in successfully.')
92
+ return redirect(url_for('auth.dashboard'))
93
+ else:
94
+ flash('Invalid email or password.')
95
+ return redirect(url_for('auth.login'))
96
+
97
+ return render_template('login.html')
98
+
99
+ @auth.route('/resend-verification', methods=['GET', 'POST'])
100
+ def resend_verification():
101
+ if request.method == 'POST':
102
+ email = request.form['email']
103
+ user = User.query.filter_by(email=email).first()
104
+
105
+ if user:
106
+ if user.is_verified:
107
+ flash('Your email is already verified. You can log in.')
108
+ return redirect(url_for('auth.login'))
109
+
110
+ user.generate_verification_token()
111
+ db.session.commit()
112
+ if send_verification_email(user.email, user.username, user.verification_token):
113
+ flash('Verification email resent. Please check your inbox.')
114
+ else:
115
+ flash('Failed to send verification email. Try again later.')
116
+ else:
117
+ flash('No account found with that email.')
118
+
119
+ return redirect(url_for('auth.login'))
120
+
121
+ return render_template('resend_verification.html')
122
+
123
+
124
+ @auth.route('/dashboard')
125
+ def dashboard():
126
+ if 'user_id' not in session:
127
+ flash('Please log in to access the dashboard.')
128
+ return redirect(url_for('auth.login'))
129
+
130
+ user = User.query.get(session['user_id'])
131
+ return render_template('dashboard.html', user=user)
132
+
133
+ @auth.route('/generate-api', methods=['GET', 'POST'])
134
+ def generate_api():
135
+ if 'user_id' not in session:
136
+ flash('Please log in to access the API generator.')
137
+ return redirect(url_for('auth.login'))
138
+
139
+ if request.method == 'POST':
140
+ flask_code, html_code = generate_ranker_code()
141
+ return render_template('generated_code.html', flask_code=flask_code, html_code=html_code)
142
+
143
+ return render_template('generate_api.html')
144
+
145
+ @auth.route('/regenerate-api-key', methods=['POST'])
146
+ def regenerate_api_key():
147
+ if 'user_id' not in session:
148
+ flash('Please log in.')
149
+ return redirect(url_for('auth.login'))
150
+
151
+ user = User.query.get(session['user_id'])
152
+ user.generate_api_key()
153
+ db.session.commit()
154
+ flash('API key regenerated.')
155
+ return redirect(url_for('auth.dashboard'))
156
+
157
+ @auth.route('/logout')
158
+ def logout():
159
+ session.clear()
160
+ flash('You have been logged out.')
161
+ return redirect(url_for('auth.login'))
162
+
163
+ @auth.route('/verify/<token>')
164
+ def verify_email(token):
165
+ user = User.query.filter_by(verification_token=token).first()
166
+ if user:
167
+ user.is_verified = True
168
+ user.verification_token = None
169
+ db.session.commit()
170
+ flash('Email verified! You can now log in.')
171
+ return redirect(url_for('auth.login'))
172
+ else:
173
+ flash('Invalid or expired verification link.')
174
+ return redirect(url_for('auth.register'))
175
+
176
+ @auth.route('/docs')
177
+ def docs():
178
+ return render_template('docs.html', response=None)
179
+
180
+ # Inside your auth.py or views.py
181
+
182
+ from flask import request, render_template, redirect, url_for, flash
183
+ from .models import User
184
+ from .utils import rank_resumes_by_similarity # import your real function
185
+
186
+ @auth.route('/try-api', methods=['POST'])
187
+ def try_api():
188
+ api_key = request.form.get('api_key')
189
+ resumes = request.files.getlist('resumes')
190
+ job_description = request.form.get('job_description')
191
+
192
+ if not api_key or not resumes or not job_description:
193
+ flash("All fields are required.")
194
+ return redirect(url_for('auth.docs'))
195
+
196
+ user = User.query.filter_by(api_key=api_key).first()
197
+ if not user:
198
+ return render_template('docs.html', response=" Invalid API key.")
199
+
200
+ try:
201
+ # ✅ Use the actual ranking logic
202
+ results = rank_resumes_by_semantic_similarity(resumes, job_description)
203
+
204
+ return render_template(
205
+ 'docs.html',
206
+ response=results,
207
+ job_description=job_description
208
+ )
209
+ except Exception as e:
210
+ return render_template('docs.html', response=f" Error: {str(e)}")
app/routes/api.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # routes/api.py
2
+ from flask import Blueprint, request, jsonify
3
+ from ..ranker import rank_resumes_by_semantic_similarity # <-- updated import
4
+ from ..decorators import require_api_key
5
+
6
+ api = Blueprint('api', __name__)
7
+
8
+ @api.route('/api/rank-resumes', methods=['POST'])
9
+ @require_api_key
10
+ def rank_resumes():
11
+ job_description = request.form.get('job_description')
12
+ resume_files = request.files.getlist('resumes')
13
+
14
+ if not job_description or not resume_files:
15
+ return jsonify({'error': 'Missing job description or resumes'}), 400
16
+
17
+ try:
18
+ results = rank_resumes_by_semantic_similarity(resume_files, job_description)
19
+ return jsonify({
20
+ 'results': results,
21
+ 'count': len(results),
22
+ 'requested_by': request.user.email
23
+ })
24
+ except Exception as e:
25
+ return jsonify({'error': str(e)}), 500
app/templates/base.html ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>{% block title %}Auth App{% endblock %}</title>
5
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
6
+ </head>
7
+ <body class="container mt-5">
8
+
9
+ {% with messages = get_flashed_messages() %}
10
+ {% if messages %}
11
+ <div class="alert alert-info">
12
+ {% for message in messages %}
13
+ <div>{{ message }}</div>
14
+ {% endfor %}
15
+ </div>
16
+ {% endif %}
17
+ {% endwith %}
18
+
19
+ {% block content %}{% endblock %}
20
+
21
+ </body>
22
+ </html>
app/templates/dashboard.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Dashboard{% endblock %}
3
+ {% block content %}
4
+ <h2>Welcome, {{ user.username }}!</h2>
5
+ <p>This is your protected dashboard.</p>
6
+ <h5>Your API Key</h5>
7
+ <div class="input-group mb-3">
8
+ <input type="text" class="form-control" id="apiKey" value="{{ user.api_key }}" readonly>
9
+ <button class="btn btn-outline-secondary" type="button" onclick="copyApiKey()">Copy</button>
10
+ </div>
11
+
12
+ <form method="POST" action="{{ url_for('auth.regenerate_api_key') }}">
13
+ <button class="btn btn-warning">Regenerate API Key</button>
14
+ </form>
15
+ <div>
16
+ <p><a href="{{ url_for('auth.docs') }}">📘 View API Documentation</a></p>
17
+
18
+ </div>
19
+ <!-- <p><a href="{{ url_for('auth.docs') }}">📘 View API Documentation</a></p> -->
20
+
21
+ <a href="{{ url_for('auth.logout') }}" class="btn btn-danger">Logout</a>
22
+
23
+ <script>
24
+ function copyApiKey() {
25
+ const input = document.getElementById("apiKey");
26
+ input.select();
27
+ input.setSelectionRange(0, 99999); // For mobile
28
+ document.execCommand("copy");
29
+ alert("API key copied to clipboard!");
30
+ }
31
+ </script>
32
+
33
+ {% endblock %}
34
+
app/templates/docs.html ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}API Documentation{% endblock %}
3
+
4
+ {% block content %}
5
+ <h2 class="mb-4">📘 Resume Ranker API Documentation</h2>
6
+
7
+ <h4>🔗 Endpoint</h4>
8
+ <pre><code class="copyable">POST https://yourdomain.com/api/rank-resumes</code></pre>
9
+
10
+ <h4>🔐 Authentication</h4>
11
+ <p>Include your API key in the <code>Authorization</code> header:</p>
12
+ <pre><code class="copyable">Authorization: Bearer amn=your_api_key_here</code></pre>
13
+
14
+ <h4>📤 Request Parameters</h4>
15
+ <ul>
16
+ <li><strong>resumes</strong> (file[], required): One or more PDF resumes</li>
17
+ <li><strong>job_description</strong> (text, required): The job description to match against</li>
18
+ </ul>
19
+
20
+ <h4>📥 Example Request (curl)</h4>
21
+ <pre><code class="copyable">curl -X POST https://yourdomain.com/api/rank-resumes \
22
+ -H "Authorization: Bearer amn=your_api_key_here" \
23
+ -F "resumes=@cv1.pdf" \
24
+ -F "resumes=@cv2.pdf" \
25
+ -F "job_description=We are hiring a backend engineer..."</code></pre>
26
+
27
+ <h4>📥 Example Request (Python)</h4>
28
+ <pre><code class="copyable">import requests
29
+
30
+ headers = {
31
+ "Authorization": "Bearer amn=your_api_key_here"
32
+ }
33
+ files = [
34
+ ('resumes', ('cv1.pdf', open('cv1.pdf', 'rb'), 'application/pdf')),
35
+ ('resumes', ('cv2.pdf', open('cv2.pdf', 'rb'), 'application/pdf')),
36
+ ]
37
+ data = {
38
+ "job_description": "We are hiring a backend engineer..."
39
+ }
40
+
41
+ response = requests.post("https://yourdomain.com/api/rank-resumes", headers=headers, files=files, data=data)
42
+ print(response.json())</code></pre>
43
+
44
+ <h4>📦 Sample Response</h4>
45
+ <pre><code class="copyable">{
46
+ "results": [
47
+ {
48
+ "filename": "cv2.pdf",
49
+ "score": 0.8745
50
+ },
51
+ {
52
+ "filename": "cv1.pdf",
53
+ "score": 0.6721
54
+ }
55
+ ],
56
+ "count": 2,
57
+ "requested_by": "user@example.com"
58
+ }</code></pre>
59
+
60
+ <hr class="my-5">
61
+
62
+ <h4>🧪 Try It Now</h4>
63
+ <form method="POST" action="{{ url_for('auth.try_api') }}" enctype="multipart/form-data">
64
+ <div class="mb-3">
65
+ <label for="api_key" class="form-label">API Key</label>
66
+ <input type="text" class="form-control" id="api_key" name="api_key" placeholder="amn=your_api_key_here" required>
67
+ </div>
68
+ <div class="mb-3">
69
+ <label for="resumes" class="form-label">Upload Resumes (PDF)</label>
70
+ <input type="file" class="form-control" id="resumes" name="resumes" multiple required>
71
+ </div>
72
+ <div class="mb-3">
73
+ <label for="job_description" class="form-label">Job Description</label>
74
+ <textarea class="form-control" id="job_description" name="job_description" rows="4" required></textarea>
75
+ </div>
76
+ <button type="submit" class="btn btn-success">Submit Test Request</button>
77
+ </form>
78
+
79
+ {% if response is iterable and response[0] is mapping %}
80
+ <hr class="my-5">
81
+ <h5>📬 API Response</h5>
82
+ <table class="table table-striped">
83
+ <thead>
84
+ <tr><th>Filename</th><th>Score</th></tr>
85
+ </thead>
86
+ <tbody>
87
+ {% for item in response %}
88
+ <tr>
89
+ <td>{{ item.filename }}</td>
90
+ <td>{{ item.score }}</td>
91
+ </tr>
92
+ {% endfor %}
93
+ </tbody>
94
+ </table>
95
+ {% elif response %}
96
+ <hr class="my-5">
97
+ <h5>📬 API Response</h5>
98
+ <pre><code>{{ response }}</code></pre>
99
+ {% endif %}
100
+
101
+ <p class="mt-4"><a href="{{ url_for('auth.dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a></p>
102
+
103
+ <!-- ✅ Copy Button Script -->
104
+ <script>
105
+ document.querySelectorAll('code.copyable').forEach(block => {
106
+ const button = document.createElement('button');
107
+ button.innerText = 'Copy';
108
+ button.className = 'btn btn-sm btn-outline-primary mb-2 float-end';
109
+ button.addEventListener('click', () => {
110
+ const text = block.innerText;
111
+ navigator.clipboard.writeText(text);
112
+ button.innerText = 'Copied!';
113
+ setTimeout(() => button.innerText = 'Copy', 1500);
114
+ });
115
+ block.parentElement.insertBefore(button, block);
116
+ });
117
+ </script>
118
+ {% endblock %}
app/templates/generate_api.html ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Generate Resume Ranker API{% endblock %}
3
+ {% block content %}
4
+ <h2>Generate Resume Ranker API</h2>
5
+ <p>This will generate a single endpoint that accepts resumes and a job description, and returns ranked results.</p>
6
+ <form method="POST">
7
+ <button type="submit" class="btn btn-primary">Generate API</button>
8
+ </form>
9
+ {% endblock %}
app/templates/generated_code.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Generated Code{% endblock %}
3
+ {% block content %}
4
+ <h2>Generated Flask Code</h2>
5
+ <pre><code>{{ flask_code }}</code></pre>
6
+
7
+ {% if html_code %}
8
+ <h2>Generated HTML Form</h2>
9
+ <pre><code>{{ html_code }}</code></pre>
10
+ {% endif %}
11
+
12
+ <a href="{{ url_for('auth.generate_api') }}" class="btn btn-secondary mt-3">Generate Another</a>
13
+ {% endblock %}
app/templates/login.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Login{% endblock %}
3
+ {% block content %}
4
+ <h2>Login</h2>
5
+ <form method="POST">
6
+ <div class="mb-3">
7
+ <input name="email" type="email" class="form-control" placeholder="Email" required>
8
+ </div>
9
+ <div class="mb-3">
10
+ <input name="password" type="password" class="form-control" placeholder="Password" required>
11
+ </div>
12
+ <button type="submit" class="btn btn-primary">Login</button>
13
+ </form>
14
+ <p class="mt-3">
15
+ Didn't get the verification email?
16
+ <a href="{{ url_for('auth.resend_verification') }}">Resend it</a>
17
+ </p>
18
+
19
+ <p class="mt-3">Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a></p>
20
+ {% endblock %}
app/templates/register.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Register{% endblock %}
3
+ {% block content %}
4
+ <h2>Register</h2>
5
+ <form method="POST">
6
+ <div class="mb-3">
7
+ <input name="username" class="form-control" placeholder="Username" required>
8
+ </div>
9
+ <div class="mb-3">
10
+ <input name="email" type="email" class="form-control" placeholder="Email" required>
11
+ </div>
12
+ <div class="mb-3">
13
+ <input name="password" type="password" class="form-control" placeholder="Password" required>
14
+ </div>
15
+ <button type="submit" class="btn btn-success">Register</button>
16
+ </form>
17
+ <p class="mt-3">Already have an account? <a href="{{ url_for('auth.login') }}">Login</a></p>
18
+ {% endblock %}
app/templates/resend_verification.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Resend Verification{% endblock %}
3
+ {% block content %}
4
+ <h2>Resend Verification Email</h2>
5
+ <form method="POST">
6
+ <div class="mb-3">
7
+ <input name="email" type="email" class="form-control" placeholder="Enter your email" required>
8
+ </div>
9
+ <button type="submit" class="btn btn-warning">Resend Email</button>
10
+ </form>
11
+ <p class="mt-3"><a href="{{ url_for('auth.login') }}">Back to login</a></p>
12
+ {% endblock %}
app/utils.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fitz # PyMuPDF
2
+ from sklearn.feature_extraction.text import TfidfVectorizer
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+
5
+ def extract_text_from_pdf(file_storage):
6
+ with fitz.open(stream=file_storage.read(), filetype="pdf") as doc:
7
+ return " ".join([page.get_text() for page in doc])
8
+
9
+ def rank_resumes_by_similarity(resume_files, job_description):
10
+ resume_texts = [extract_text_from_pdf(f) for f in resume_files]
11
+ documents = [job_description] + resume_texts
12
+
13
+ vectorizer = TfidfVectorizer(stop_words='english')
14
+ tfidf_matrix = vectorizer.fit_transform(documents)
15
+
16
+ job_vector = tfidf_matrix[0]
17
+ resume_vectors = tfidf_matrix[1:]
18
+
19
+ similarities = cosine_similarity(job_vector, resume_vectors).flatten()
20
+ ranked = sorted(zip(resume_files, similarities), key=lambda x: x[1], reverse=True)
21
+
22
+ return [
23
+ {"filename": file.filename, "score": round(score, 4)}
24
+ for file, score in ranked
25
+ ]
26
+
27
+
28
+ # utils/auth.py
29
+ from functools import wraps
30
+ from flask import request, jsonify
31
+ from .models import User # adjust import to your structure
32
+
33
+ def require_api_key(f):
34
+ @wraps(f)
35
+ def decorated_function(*args, **kwargs):
36
+ auth_header = request.headers.get('Authorization')
37
+
38
+ if not auth_header or not auth_header.startswith('Bearer '):
39
+ return jsonify({"error": "Missing or invalid Authorization header."}), 401
40
+
41
+ token = auth_header.split("Bearer ")[1].strip()
42
+
43
+ user = User.query.filter_by(api_key=token).first()
44
+ if not user:
45
+ return jsonify({"error": "Invalid or revoked API key."}), 403
46
+
47
+ request.user = user # attach user to request for downstream use
48
+ return f(*args, **kwargs)
49
+ return decorated_function
dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ COPY requirements.txt .
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy all files
10
+ COPY . .
11
+
12
+ # Optional: avoid using cached Hugging Face models in root directory
13
+ ENV TRANSFORMERS_CACHE=/app/cache
14
+ ENV HF_HOME=/app/cache
15
+
16
+ # Set PYTHONPATH (useful if you're importing from app/)
17
+ ENV PYTHONPATH=/app
18
+
19
+ # HF Spaces default port is 7860
20
+ EXPOSE 7860
21
+
22
+ # Start server
23
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "main:app"]
main.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import create_app
2
+ from dotenv import load_dotenv
3
+ import os
4
+
5
+ load_dotenv()
6
+
7
+ app = create_app()
8
+
9
+ if __name__ == '__main__':
10
+ port = int(os.environ.get("PORT", 7860))
11
+ app.run(host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Flask
2
+ Flask-SQLAlchemy
3
+ Flask-Migrate
4
+ Werkzeug
5
+ python-dotenv
6
+ PyMuPDF
7
+ scikit-learn
8
+ requests
9
+ sentence-transformers
10
+ gunicorn
11
+ transformers
12
+ torch