Prune unverified accounts

This commit is contained in:
2025-11-22 10:44:50 +01:00
parent 87471e033e
commit 983d10ea97
5 changed files with 100 additions and 74 deletions

58
app.py
View File

@@ -102,14 +102,14 @@ def force_http_scheme():
if app.debug or os.environ.get('FLASK_ENV') == 'debug':
from flask import url_for as original_url_for
import functools
@functools.wraps(original_url_for)
def url_for_http(*args, **kwargs):
# Force _scheme to http if _external is True
if kwargs.get('_external'):
kwargs['_scheme'] = 'http'
return original_url_for(*args, **kwargs)
app.jinja_env.globals['url_for'] = url_for_http
# Configure Flask-Mail
@@ -365,7 +365,7 @@ def robots_txt():
def sitemap_xml():
"""Generate XML sitemap for search engines"""
pages = []
# Static pages accessible without login
static_pages = [
{'loc': '/', 'priority': '1.0', 'changefreq': 'daily'},
@@ -373,7 +373,7 @@ def sitemap_xml():
{'loc': '/register', 'priority': '0.9', 'changefreq': 'monthly'},
{'loc': '/forgot_password', 'priority': '0.5', 'changefreq': 'monthly'},
]
for page in static_pages:
pages.append({
'loc': request.host_url[:-1] + page['loc'],
@@ -381,10 +381,10 @@ def sitemap_xml():
'priority': page['priority'],
'changefreq': page['changefreq']
})
sitemap_xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
sitemap_xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
for page in pages:
sitemap_xml += ' <url>\n'
sitemap_xml += f' <loc>{page["loc"]}</loc>\n'
@@ -392,9 +392,9 @@ def sitemap_xml():
sitemap_xml += f' <changefreq>{page["changefreq"]}</changefreq>\n'
sitemap_xml += f' <priority>{page["priority"]}</priority>\n'
sitemap_xml += ' </url>\n'
sitemap_xml += '</urlset>'
return Response(sitemap_xml, mimetype='application/xml')
@app.route('/site.webmanifest')
@@ -986,11 +986,11 @@ def forgot_password():
"""Handle forgot password requests"""
if request.method == 'POST':
username_or_email = request.form.get('username_or_email', '').strip()
if not username_or_email:
flash('Please enter your username or email address.', 'error')
return render_template('forgot_password.html', title='Forgot Password')
# Try to find user by username or email
user = User.query.filter(
db.or_(
@@ -998,11 +998,11 @@ def forgot_password():
User.email == username_or_email
)
).first()
if user and user.email:
# Generate reset token
token = user.generate_password_reset_token()
# Send reset email
reset_url = url_for('reset_password', token=token, _external=True)
msg = Message(
@@ -1023,7 +1023,7 @@ If you did not request a password reset, please ignore this email.
Best regards,
The {g.branding.app_name if g.branding else "TimeTrack"} Team
'''
try:
mail.send(msg)
logger.info(f"Password reset email sent to user {user.username}")
@@ -1031,11 +1031,11 @@ The {g.branding.app_name if g.branding else "TimeTrack"} Team
logger.error(f"Failed to send password reset email: {str(e)}")
flash('Failed to send reset email. Please contact support.', 'error')
return render_template('forgot_password.html', title='Forgot Password')
# Always show success message to prevent user enumeration
flash('If an account exists with that username or email address, we have sent a password reset link.', 'success')
return redirect(url_for('login'))
return render_template('forgot_password.html', title='Forgot Password')
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
@@ -1043,42 +1043,42 @@ def reset_password(token):
"""Handle password reset with token"""
# Find user by reset token
user = User.query.filter_by(password_reset_token=token).first()
if not user or not user.verify_password_reset_token(token):
flash('Invalid or expired reset link.', 'error')
return redirect(url_for('login'))
if request.method == 'POST':
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# Validate input
error = None
if not password:
error = 'Password is required'
elif password != confirm_password:
error = 'Passwords do not match'
# Validate password strength
if not error:
validator = PasswordValidator()
is_valid, password_errors = validator.validate(password)
if not is_valid:
error = password_errors[0]
if error:
flash(error, 'error')
return render_template('reset_password.html', token=token, title='Reset Password')
# Update password
user.set_password(password)
user.clear_password_reset_token()
db.session.commit()
logger.info(f"Password reset successful for user {user.username}")
flash('Your password has been reset successfully. Please log in with your new password.', 'success')
return redirect(url_for('login'))
return render_template('reset_password.html', token=token, title='Reset Password')
@app.route('/dashboard')
@@ -2931,14 +2931,14 @@ def render_markdown():
try:
data = request.get_json()
content = data.get('content', '')
if not content:
return jsonify({'html': '<p class="preview-placeholder">Start typing to see the preview...</p>'})
# Parse frontmatter and extract body
from frontmatter_utils import parse_frontmatter
metadata, body = parse_frontmatter(content)
# Render markdown to HTML
try:
import markdown
@@ -2947,13 +2947,13 @@ def render_markdown():
except ImportError:
# Fallback if markdown not installed
html = f'<pre>{body}</pre>'
return jsonify({'html': html})
except Exception as e:
logger.error(f"Error rendering markdown: {str(e)}")
return jsonify({'html': '<p class="error">Error rendering markdown</p>'})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run(debug=True, host='0.0.0.0', port=port)
app.run(debug=True, host='0.0.0.0', port=port)