Merge feature/inshorturl-fullstack-implementation-flz into main#7
Conversation
Reviewer's GuideThis PR introduces a full-stack URL shortener feature (inShortUrl) by adding a Flask backend with SQLAlchemy models and utilities, accompanied by a vanilla JavaScript/CSS/HTML frontend under a new url_shortener directory; it also includes a minor dependency correction in the root requirements. Class diagram for the new URL model in inShortUrlclassDiagram
class URL {
+id: Integer
+original_url: String
+short_code: String
+created_at: DateTime
+clicks: Integer
+to_dict()
}
class Base
URL --|> Base: inherits
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes and they look great!
Blocking issues:
- User controlled data in methods like
innerHTML,outerHTMLordocument.writeis an anti-pattern that can lead to XSS vulnerabilities (link) - User controlled data in a
recentList.innerHTMLis an anti-pattern that can lead to XSS vulnerabilities (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `url_shortener/app.py:128` </location>
<code_context>
+ return jsonify({'error': 'Internal server error'}), 500
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=5000, debug=True)
</code_context>
<issue_to_address>
**🚨 issue (security):** Debug mode is enabled in production entrypoint.
Debug mode should be disabled in production to prevent exposing sensitive information. Make debug configurable via environment variable and default to False.
</issue_to_address>
### Comment 2
<location> `url_shortener/models.py:27-31` </location>
<code_context>
+ 'clicks': self.clicks
+ }
+
+def init_db(database_url='sqlite:///urls.db'):
+ engine = create_engine(database_url)
+ Base.metadata.create_all(engine)
+ Session = sessionmaker(bind=engine)
+ return Session()
</code_context>
<issue_to_address>
**issue (bug_risk):** init_db returns a session instance, not a session factory.
Reusing a single session across requests can lead to concurrency problems. Refactor init_db to return the sessionmaker instead, and create a new session for each request.
</issue_to_address>
### Comment 3
<location> `url_shortener/static/script.js:87-97` </location>
<code_context>
+}
+
+function copyToClipboard() {
+ shortUrlInput.select();
+ document.execCommand('copy');
+
+ const originalText = copyBtn.textContent;
</code_context>
<issue_to_address>
**suggestion:** Clipboard copy uses deprecated execCommand API.
Switch to navigator.clipboard.writeText for improved browser support and to avoid deprecated methods.
```suggestion
navigator.clipboard.writeText(shortUrlInput.value).then(() => {
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.classList.remove('copied');
}, 2000);
});
```
</issue_to_address>
### Comment 4
<location> `url_shortener/static/script.js:141-150` </location>
<code_context>
recentList.innerHTML = urls.map(url => `
<div class="recent-item">
<div class="recent-item-header">
<span class="recent-item-code">/${url.short_code}</span>
<span class="recent-item-clicks">👁️ ${url.clicks} clicks</span>
</div>
<div class="recent-item-url">${truncateUrl(url.original_url, 60)}</div>
<div class="recent-item-date">${formatDate(url.created_at)}</div>
</div>
`).join('');
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-document-method):** User controlled data in methods like `innerHTML`, `outerHTML` or `document.write` is an anti-pattern that can lead to XSS vulnerabilities
*Source: opengrep*
</issue_to_address>
### Comment 5
<location> `url_shortener/static/script.js:141-150` </location>
<code_context>
recentList.innerHTML = urls.map(url => `
<div class="recent-item">
<div class="recent-item-header">
<span class="recent-item-code">/${url.short_code}</span>
<span class="recent-item-clicks">👁️ ${url.clicks} clicks</span>
</div>
<div class="recent-item-url">${truncateUrl(url.original_url, 60)}</div>
<div class="recent-item-date">${formatDate(url.created_at)}</div>
</div>
`).join('');
</code_context>
<issue_to_address>
**security (javascript.browser.security.insecure-innerhtml):** User controlled data in a `recentList.innerHTML` is an anti-pattern that can lead to XSS vulnerabilities
*Source: opengrep*
</issue_to_address>
### Comment 6
<location> `url_shortener/app.py:45-52` </location>
<code_context>
@app.route('/api/shorten', methods=['POST'])
def shorten_url():
"""Create a shortened URL"""
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'URL is required'}), 400
original_url = data['url']
custom_code = data.get('custom_code', '').strip()
# Validate URL
if not is_valid_url(original_url):
return jsonify({'error': 'Invalid URL format'}), 400
# Check if custom code is provided
if custom_code:
# Validate custom code (alphanumeric only, 3-10 chars)
if not custom_code.isalnum() or len(custom_code) < 3 or len(custom_code) > 10:
return jsonify({'error': 'Custom code must be 3-10 alphanumeric characters'}), 400
# Check if custom code already exists
existing = db_session.query(URL).filter_by(short_code=custom_code).first()
if existing:
return jsonify({'error': 'Custom code already taken'}), 409
short_code = custom_code
else:
# Check if URL already exists
existing_url = db_session.query(URL).filter_by(original_url=original_url).first()
if existing_url:
return jsonify({
'short_url': f"{BASE_URL}/{existing_url.short_code}",
'short_code': existing_url.short_code,
'original_url': existing_url.original_url,
'created_at': existing_url.created_at.isoformat(),
'clicks': existing_url.clicks
}), 200
# Generate unique short code
short_code = generate_short_code()
while db_session.query(URL).filter_by(short_code=short_code).first():
short_code = generate_short_code()
# Create new URL entry
new_url = URL(original_url=original_url, short_code=short_code)
db_session.add(new_url)
db_session.commit()
return jsonify({
'short_url': f"{BASE_URL}/{short_code}",
'short_code': short_code,
'original_url': original_url,
'created_at': new_url.created_at.isoformat(),
'clicks': 0
}), 201
</code_context>
<issue_to_address>
**issue (code-quality):** Use named expression to simplify assignment and conditional [×2] ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
</issue_to_address>
### Comment 7
<location> `url_shortener/app.py:97-100` </location>
<code_context>
@app.route('/api/stats/<short_code>')
def get_stats(short_code):
"""Get statistics for a shortened URL"""
url_entry = db_session.query(URL).filter_by(short_code=short_code).first()
if not url_entry:
return jsonify({'error': 'Short URL not found'}), 404
return jsonify(url_entry.to_dict()), 200
</code_context>
<issue_to_address>
**issue (code-quality):** We've found these issues:
- Use named expression to simplify assignment and conditional ([`use-named-expression`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/use-named-expression/))
- Lift code into else after jump in control flow ([`reintroduce-else`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/reintroduce-else/))
- Swap if/else branches ([`swap-if-else-branches`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/swap-if-else-branches/))
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| return jsonify({'error': 'Internal server error'}), 500 | ||
|
|
||
| if __name__ == '__main__': | ||
| app.run(host='0.0.0.0', port=5000, debug=True) |
There was a problem hiding this comment.
🚨 issue (security): Debug mode is enabled in production entrypoint.
Debug mode should be disabled in production to prevent exposing sensitive information. Make debug configurable via environment variable and default to False.
| def init_db(database_url='sqlite:///urls.db'): | ||
| engine = create_engine(database_url) | ||
| Base.metadata.create_all(engine) | ||
| Session = sessionmaker(bind=engine) | ||
| return Session() |
There was a problem hiding this comment.
issue (bug_risk): init_db returns a session instance, not a session factory.
Reusing a single session across requests can lead to concurrency problems. Refactor init_db to return the sessionmaker instead, and create a new session for each request.
| shortUrlInput.select(); | ||
| document.execCommand('copy'); | ||
|
|
||
| const originalText = copyBtn.textContent; | ||
| copyBtn.textContent = 'Copied!'; | ||
| copyBtn.classList.add('copied'); | ||
|
|
||
| setTimeout(() => { | ||
| copyBtn.textContent = originalText; | ||
| copyBtn.classList.remove('copied'); | ||
| }, 2000); |
There was a problem hiding this comment.
suggestion: Clipboard copy uses deprecated execCommand API.
Switch to navigator.clipboard.writeText for improved browser support and to avoid deprecated methods.
| shortUrlInput.select(); | |
| document.execCommand('copy'); | |
| const originalText = copyBtn.textContent; | |
| copyBtn.textContent = 'Copied!'; | |
| copyBtn.classList.add('copied'); | |
| setTimeout(() => { | |
| copyBtn.textContent = originalText; | |
| copyBtn.classList.remove('copied'); | |
| }, 2000); | |
| navigator.clipboard.writeText(shortUrlInput.value).then(() => { | |
| const originalText = copyBtn.textContent; | |
| copyBtn.textContent = 'Copied!'; | |
| copyBtn.classList.add('copied'); | |
| setTimeout(() => { | |
| copyBtn.textContent = originalText; | |
| copyBtn.classList.remove('copied'); | |
| }, 2000); | |
| }); |
| recentList.innerHTML = urls.map(url => ` | ||
| <div class="recent-item"> | ||
| <div class="recent-item-header"> | ||
| <span class="recent-item-code">/${url.short_code}</span> | ||
| <span class="recent-item-clicks">👁️ ${url.clicks} clicks</span> | ||
| </div> | ||
| <div class="recent-item-url">${truncateUrl(url.original_url, 60)}</div> | ||
| <div class="recent-item-date">${formatDate(url.created_at)}</div> | ||
| </div> | ||
| `).join(''); |
There was a problem hiding this comment.
security (javascript.browser.security.insecure-document-method): User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities
Source: opengrep
| recentList.innerHTML = urls.map(url => ` | ||
| <div class="recent-item"> | ||
| <div class="recent-item-header"> | ||
| <span class="recent-item-code">/${url.short_code}</span> | ||
| <span class="recent-item-clicks">👁️ ${url.clicks} clicks</span> | ||
| </div> | ||
| <div class="recent-item-url">${truncateUrl(url.original_url, 60)}</div> | ||
| <div class="recent-item-date">${formatDate(url.created_at)}</div> | ||
| </div> | ||
| `).join(''); |
There was a problem hiding this comment.
security (javascript.browser.security.insecure-innerhtml): User controlled data in a recentList.innerHTML is an anti-pattern that can lead to XSS vulnerabilities
Source: opengrep
| existing = db_session.query(URL).filter_by(short_code=custom_code).first() | ||
| if existing: | ||
| return jsonify({'error': 'Custom code already taken'}), 409 | ||
|
|
||
| short_code = custom_code | ||
| else: | ||
| # Check if URL already exists | ||
| existing_url = db_session.query(URL).filter_by(original_url=original_url).first() |
There was a problem hiding this comment.
issue (code-quality): Use named expression to simplify assignment and conditional [×2] (use-named-expression)
| url_entry = db_session.query(URL).filter_by(short_code=short_code).first() | ||
|
|
||
| if not url_entry: | ||
| return jsonify({'error': 'Short URL not found'}), 404 |
There was a problem hiding this comment.
issue (code-quality): We've found these issues:
- Use named expression to simplify assignment and conditional (
use-named-expression) - Lift code into else after jump in control flow (
reintroduce-else) - Swap if/else branches (
swap-if-else-branches)
Automated PR created from task completion
This PR was created from task: https://cloud.blackbox.ai/tasks/VOFCUkun8E1riQnxKI4p9
Summary by Sourcery
Merge full-stack URL shortener feature into main, introducing a Flask backend with SQLAlchemy, RESTful endpoints, utility modules, static frontend assets, and project documentation.
New Features:
Enhancements:
Documentation: