5 Commits

Author SHA1 Message Date
6afa4619ef Add reCAPTCHA env variables to docker-compose.yml 2025-11-22 12:10:24 +01:00
57bb0f5b9e Add reCAPTCHA feature 2025-11-22 11:53:28 +01:00
8de4378ad9 Add missing cron scripts. 2025-11-22 11:12:40 +01:00
c375b9ee3d Add cron file 2025-11-22 10:52:58 +01:00
983d10ea97 Prune unverified accounts 2025-11-22 10:44:50 +01:00
14 changed files with 809 additions and 92 deletions

View File

@@ -25,3 +25,9 @@ MAIL_USE_TLS=true
MAIL_USERNAME=your-email@example.com
MAIL_PASSWORD=your-password
MAIL_DEFAULT_SENDER=TimeTrack <noreply@timetrack.com>
# reCAPTCHA Configuration (Google reCAPTCHA v2)
# Get your keys at: https://www.google.com/recaptcha/admin
RECAPTCHA_SITE_KEY=your-site-key-here
RECAPTCHA_SECRET_KEY=your-secret-key-here
RECAPTCHA_ENABLED=true

View File

@@ -39,8 +39,16 @@ docker-compose up
# Debug mode with hot-reload
docker-compose -f docker-compose.debug.yml up
# Manual cleanup of unverified accounts
docker exec timetrack-timetrack-1 python cleanup_unverified_accounts.py
# Dry run to see what would be deleted
docker exec timetrack-timetrack-1 python cleanup_unverified_accounts.py --dry-run
```
**Note:** Unverified accounts are automatically cleaned up every hour via cron job in the Docker container.
## Database Operations
### Flask-Migrate Commands
@@ -187,6 +195,7 @@ When modifying billing/invoice features:
- Two-factor authentication (2FA) using TOTP
- Session-based authentication with "Remember Me" option
- Email verification for new accounts (configurable)
- Automatic cleanup of unverified accounts after 24 hours
### Mobile UI Features
- Progressive Web App (PWA) manifest for installability

View File

@@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
python3-dev \
postgresql-client \
cron \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -35,6 +36,12 @@ RUN pip install gunicorn==21.2.0
# Copy the rest of the application
COPY . .
# Setup cron job for cleanup
COPY docker-cron /etc/cron.d/cleanup-cron
RUN chmod 0644 /etc/cron.d/cleanup-cron && \
crontab /etc/cron.d/cleanup-cron && \
touch /var/log/cron.log
# Create the SQLite database directory with proper permissions
RUN mkdir -p /app/instance && chmod 777 /app/instance

95
app.py
View File

@@ -19,6 +19,7 @@ from flask_mail import Mail, Message
from dotenv import load_dotenv
from password_utils import PasswordValidator
from werkzeug.security import check_password_hash
from recaptcha_helper import recaptcha
# Import blueprints
from routes.notes import notes_bp
@@ -129,6 +130,9 @@ logger.info(f"Mail default sender: {app.config['MAIL_DEFAULT_SENDER']}")
mail = Mail(app)
# Initialize reCAPTCHA
recaptcha.init_app(app)
# Initialize the database with the app
db.init_app(app)
@@ -652,6 +656,13 @@ def register():
if not is_valid:
error = password_errors[0] # Show first error
# Verify reCAPTCHA
if not error:
recaptcha_response = request.form.get('g-recaptcha-response')
is_valid, recaptcha_error = recaptcha.verify(recaptcha_response, request.remote_addr)
if not is_valid:
error = recaptcha_error
# Find company by code or create new one if no code provided
company = None
if company_code:
@@ -729,31 +740,47 @@ def register():
# Make first user in company an admin with full privileges
if is_first_user_in_company:
new_user.role = Role.ADMIN
new_user.is_verified = True # Auto-verify first user in company
# Removed auto-verification - all users must verify email
elif not email_verification_required:
# If email verification is disabled, auto-verify new users
new_user.is_verified = True
# Generate verification token (even if not needed, for consistency)
# Generate verification token
token = new_user.generate_verification_token()
db.session.add(new_user)
db.session.commit()
if is_first_user_in_company:
# First user in company gets admin privileges and is auto-verified
logger.info(f"First user account created in company {company.name}: {username} with admin privileges")
flash(f'Welcome! You are the first user in {company.name} and have been granted administrator privileges. You can now log in.', 'success')
elif not email_verification_required:
if not email_verification_required:
# Email verification is disabled, user can log in immediately
logger.info(f"User account created with auto-verification in company {company.name}: {username}")
flash('Registration successful! You can now log in.', 'success')
else:
# Send verification email for regular users when verification is required
# Send verification email for all users (including first user)
verification_url = url_for('verify_email', token=token, _external=True)
msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email])
msg.body = f'''Hello {username},
# Special message for first user in company
if is_first_user_in_company:
msg.body = f'''Hello {username},
Thank you for registering with {g.branding.app_name}. You are the first user in {company.name} and have been granted administrator privileges.
To complete your registration and access your account, please click on the link below:
{verification_url}
This link will expire in 24 hours.
If you did not register for {g.branding.app_name}, please ignore this email.
Best regards,
The {g.branding.app_name} Team
'''
logger.info(f"First user account created in company {company.name}: {username} with admin privileges - verification email sent")
flash(f'Welcome! You are the first user in {company.name} and have been granted administrator privileges. Please check your email to verify your account.', 'success')
else:
msg.body = f'''Hello {username},
Thank you for registering with {g.branding.app_name}. To complete your registration, please click on the link below:
@@ -766,9 +793,10 @@ If you did not register for {g.branding.app_name}, please ignore this email.
Best regards,
The {g.branding.app_name} Team
'''
logger.info(f"User account created in company {company.name}: {username} - verification email sent")
flash('Registration initiated! Please check your email to verify your account.', 'success')
mail.send(msg)
logger.info(f"Verification email sent to {email}")
flash('Registration initiated! Please check your email to verify your account.', 'success')
return redirect(url_for('login'))
except Exception as e:
@@ -815,6 +843,13 @@ def register_freelancer():
if not is_valid:
error = password_errors[0] # Show first error
# Verify reCAPTCHA
if not error:
recaptcha_response = request.form.get('g-recaptcha-response')
is_valid, recaptcha_error = recaptcha.verify(recaptcha_response, request.remote_addr)
if not is_valid:
error = recaptcha_error
# Check for existing users globally (freelancers get unique usernames/emails)
if not error:
if User.query.filter_by(username=username).first():
@@ -851,6 +886,9 @@ def register_freelancer():
db.session.add(personal_company)
db.session.flush() # Get company ID
# Check if email verification is required
email_verification_required = get_system_setting('email_verification_required', 'true') == 'true'
# Create freelancer user
new_user = User(
username=username,
@@ -859,15 +897,42 @@ def register_freelancer():
account_type=AccountType.FREELANCER,
business_name=business_name if business_name else None,
role=Role.ADMIN, # Freelancers are admins of their personal company
is_verified=True # Auto-verify freelancers
is_verified=not email_verification_required # Only auto-verify if email verification is disabled
)
new_user.set_password(password)
# Generate verification token
token = new_user.generate_verification_token()
db.session.add(new_user)
db.session.commit()
logger.info(f"Freelancer account created: {username} with personal company: {company_name}")
flash(f'Welcome {username}! Your freelancer account has been created successfully. You can now log in.', 'success')
if not email_verification_required:
# Email verification is disabled, user can log in immediately
logger.info(f"Freelancer account created with auto-verification: {username} with personal company: {company_name}")
flash(f'Welcome {username}! Your freelancer account has been created successfully. You can now log in.', 'success')
else:
# Send verification email
verification_url = url_for('verify_email', token=token, _external=True)
msg = Message(f'Verify your {g.branding.app_name} freelancer account', recipients=[email])
msg.body = f'''Hello {username},
Thank you for registering as a freelancer with {g.branding.app_name}. Your personal workspace "{company_name}" has been created.
To complete your registration and access your account, please click on the link below:
{verification_url}
This link will expire in 24 hours.
If you did not register for {g.branding.app_name}, please ignore this email.
Best regards,
The {g.branding.app_name} Team
'''
mail.send(msg)
logger.info(f"Freelancer account created: {username} with personal company: {company_name} - verification email sent")
flash(f'Welcome {username}! Your freelancer workspace has been created. Please check your email to verify your account.', 'success')
return redirect(url_for('login'))

327
cleanup_inactive_accounts.py Executable file
View File

@@ -0,0 +1,327 @@
#!/usr/bin/env python
"""
Cleanup verified but inactive user accounts.
Identifies and removes bot-created or unused accounts that:
- Are verified (is_verified = True)
- Have never logged in (no SystemEvent login records)
- Have never created time entries
- Are older than a specified number of days
This script can be run manually or scheduled via cron.
"""
import os
import sys
from datetime import datetime, timedelta
from sqlalchemy import and_, or_
# Add the application path to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import app, db
from models import User, Company, SystemEvent, TimeEntry
from models.enums import Role
def find_inactive_verified_accounts(min_age_days=30, dry_run=False):
"""
Find verified user accounts that have never been used.
Args:
min_age_days (int): Minimum age in days for accounts to be considered
dry_run (bool): If True, only show what would be deleted without actually deleting.
Returns:
tuple: (accounts_to_delete, details_list)
"""
with app.app_context():
cutoff_time = datetime.utcnow() - timedelta(days=min_age_days)
# Find verified accounts older than cutoff
verified_old_users = User.query.filter(
and_(
User.is_verified == True,
User.created_at < cutoff_time
)
).all()
inactive_accounts = []
for user in verified_old_users:
# Check if user has any time entries
time_entry_count = TimeEntry.query.filter_by(user_id=user.id).count()
# Check if user has any login events
login_event_count = SystemEvent.query.filter(
and_(
SystemEvent.user_id == user.id,
SystemEvent.event_type == 'user_login'
)
).count()
# If no time entries and no logins, this is an inactive account
if time_entry_count == 0 and login_event_count == 0:
account_age_days = (datetime.utcnow() - user.created_at).days
# Get company info
company = Company.query.get(user.company_id) if user.company_id else None
company_name = company.name if company else "Unknown"
# Check if user is the only admin in their company
is_only_admin = False
if user.role in [Role.ADMIN, Role.SYSTEM_ADMIN]:
other_admins = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id,
User.role.in_([Role.ADMIN, Role.SYSTEM_ADMIN])
)
).count()
is_only_admin = (other_admins == 0)
inactive_accounts.append({
'user': user,
'company_name': company_name,
'age_days': account_age_days,
'is_only_admin': is_only_admin,
'company': company
})
return inactive_accounts
def cleanup_inactive_accounts(min_age_days=30, dry_run=False, skip_admins=True):
"""
Delete inactive verified accounts.
Args:
min_age_days (int): Minimum age in days for accounts to be considered
dry_run (bool): If True, only show what would be deleted without actually deleting
skip_admins (bool): If True, skip accounts that are the only admin in their company
Returns:
int: Number of accounts deleted (or would be deleted in dry run mode)
"""
with app.app_context():
inactive_accounts = find_inactive_verified_accounts(min_age_days, dry_run)
deleted_count = 0
skipped_count = 0
companies_deleted = 0
print(f"\nFound {len(inactive_accounts)} inactive verified account(s) older than {min_age_days} days")
print("=" * 80)
for account_info in inactive_accounts:
user = account_info['user']
company_name = account_info['company_name']
age_days = account_info['age_days']
is_only_admin = account_info['is_only_admin']
company = account_info['company']
# Skip if this is the only admin and skip_admins is True
if is_only_admin and skip_admins:
print(f"\nSKIPPED (only admin): {user.username}")
print(f" Email: {user.email}")
print(f" Company: {company_name} (ID: {user.company_id})")
print(f" Role: {user.role.value}")
print(f" Created: {user.created_at} ({age_days} days ago)")
skipped_count += 1
continue
if dry_run:
print(f"\nWOULD DELETE: {user.username}")
print(f" Email: {user.email}")
print(f" Company: {company_name} (ID: {user.company_id})")
print(f" Role: {user.role.value}")
print(f" Created: {user.created_at} ({age_days} days ago)")
# Check if company would be deleted too
if company:
other_users = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id
)
).count()
if other_users == 0:
print(f" -> Would also delete empty company: {company_name}")
deleted_count += 1
else:
print(f"\nDELETING: {user.username}")
print(f" Email: {user.email}")
print(f" Company: {company_name} (ID: {user.company_id})")
print(f" Role: {user.role.value}")
print(f" Created: {user.created_at} ({age_days} days ago)")
# Log the deletion as a system event
try:
SystemEvent.log_event(
event_type='user_deleted',
event_category='system',
description=f'Inactive verified user {user.username} deleted (no activity for {age_days} days)',
user_id=None, # No user context for system cleanup
company_id=user.company_id
)
except Exception as e:
print(f" Warning: Could not log deletion event: {e}")
# Check if company should be deleted
if company:
other_users = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id
)
).count()
# Delete the user
db.session.delete(user)
deleted_count += 1
# If no other users, delete the company too
if other_users == 0:
print(f" -> Also deleting empty company: {company_name}")
db.session.delete(company)
companies_deleted += 1
else:
# Delete the user even if company doesn't exist
db.session.delete(user)
deleted_count += 1
if not dry_run and deleted_count > 0:
db.session.commit()
print("\n" + "=" * 80)
print(f"Successfully deleted {deleted_count} inactive account(s)")
if companies_deleted > 0:
print(f"Successfully deleted {companies_deleted} empty company/companies")
if skipped_count > 0:
print(f"Skipped {skipped_count} account(s) (only admin in company)")
elif dry_run:
print("\n" + "=" * 80)
print(f"DRY RUN: Would delete {deleted_count} inactive account(s)")
if skipped_count > 0:
print(f"Would skip {skipped_count} account(s) (only admin in company)")
else:
print("\n" + "=" * 80)
print("No inactive accounts found to delete")
if skipped_count > 0:
print(f"Skipped {skipped_count} account(s) (only admin in company)")
return deleted_count
def generate_report(min_age_days=30):
"""
Generate a detailed report of inactive accounts without deleting anything.
Args:
min_age_days (int): Minimum age in days for accounts to be considered
"""
with app.app_context():
inactive_accounts = find_inactive_verified_accounts(min_age_days, dry_run=True)
print(f"\n{'='*80}")
print(f"INACTIVE VERIFIED ACCOUNTS REPORT")
print(f"Accounts older than {min_age_days} days with no activity")
print(f"Generated: {datetime.utcnow()}")
print(f"{'='*80}\n")
if not inactive_accounts:
print("No inactive verified accounts found.")
return
# Group by company
by_company = {}
for account_info in inactive_accounts:
company_name = account_info['company_name']
if company_name not in by_company:
by_company[company_name] = []
by_company[company_name].append(account_info)
# Print summary
print(f"Total inactive accounts: {len(inactive_accounts)}")
print(f"Companies affected: {len(by_company)}\n")
# Print details by company
for company_name, accounts in sorted(by_company.items()):
print(f"\nCompany: {company_name}")
print("-" * 80)
for account_info in accounts:
user = account_info['user']
age_days = account_info['age_days']
is_only_admin = account_info['is_only_admin']
admin_warning = " [ONLY ADMIN]" if is_only_admin else ""
print(f"{user.username}{admin_warning}")
print(f" Email: {user.email or 'N/A'}")
print(f" Role: {user.role.value}")
print(f" Created: {user.created_at} ({age_days} days ago)")
print()
def main():
"""Main function to handle command line arguments"""
import argparse
parser = argparse.ArgumentParser(
description='Cleanup verified but inactive user accounts (no logins, no time entries)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without actually deleting'
)
parser.add_argument(
'--min-age',
type=int,
default=30,
help='Minimum account age in days (default: 30)'
)
parser.add_argument(
'--include-admins',
action='store_true',
help='Include accounts that are the only admin in their company (default: skip them)'
)
parser.add_argument(
'--report-only',
action='store_true',
help='Generate a detailed report only, do not delete anything'
)
parser.add_argument(
'--quiet',
action='store_true',
help='Suppress output except for errors'
)
args = parser.parse_args()
if not args.quiet:
print(f"Starting cleanup of inactive verified accounts at {datetime.utcnow()}")
print("-" * 80)
try:
if args.report_only:
generate_report(min_age_days=args.min_age)
else:
deleted_count = cleanup_inactive_accounts(
min_age_days=args.min_age,
dry_run=args.dry_run,
skip_admins=not args.include_admins
)
if not args.quiet:
print("-" * 80)
print(f"Cleanup completed at {datetime.utcnow()}")
# Exit with 0 for success
sys.exit(0)
except Exception as e:
print(f"Error during cleanup: {str(e)}", file=sys.stderr)
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

152
cleanup_unverified_accounts.py Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python
"""
Cleanup unverified user accounts older than 24 hours.
This script can be run manually or scheduled via cron.
"""
import os
import sys
from datetime import datetime, timedelta
from sqlalchemy import and_
# Add the application path to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import app, db
from models import User, Company, SystemEvent
from models.enums import Role
def cleanup_unverified_accounts(dry_run=False):
"""
Delete unverified user accounts that are older than 24 hours.
Args:
dry_run (bool): If True, only show what would be deleted without actually deleting.
Returns:
int: Number of accounts deleted (or would be deleted in dry run mode)
"""
with app.app_context():
# Find unverified accounts older than 24 hours
cutoff_time = datetime.utcnow() - timedelta(hours=24)
unverified_users = User.query.filter(
and_(
User.is_verified == False,
User.created_at < cutoff_time
)
).all()
deleted_count = 0
for user in unverified_users:
# Check if this user is the only admin in their company
# We shouldn't delete them if they are, as it would orphan the company
if user.role in [Role.ADMIN, Role.SYSTEM_ADMIN]:
other_admins = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id,
User.role.in_([Role.ADMIN, Role.SYSTEM_ADMIN])
)
).count()
if other_admins == 0:
print(f"Skipping {user.username} (ID: {user.id}) - only admin in company {user.company_id}")
continue
if dry_run:
print(f"Would delete unverified user: {user.username} (ID: {user.id}, Email: {user.email}, Created: {user.created_at})")
deleted_count += 1
# Check if company would be deleted too (for dry run)
company = Company.query.get(user.company_id)
if company:
other_users = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id
)
).count()
if other_users == 0:
print(f" Would also delete empty company: {company.name} (ID: {company.id})")
else:
print(f"Deleting unverified user: {user.username} (ID: {user.id}, Email: {user.email}, Created: {user.created_at})")
# Log the deletion as a system event if SystemEvent exists
try:
SystemEvent.log_event(
event_type='user_deleted',
event_category='system',
description=f'Unverified user {user.username} deleted after 24 hours',
user_id=None, # No user context for system cleanup
company_id=user.company_id
)
except:
# SystemEvent might not exist, continue without logging
pass
# Check if the company should be deleted (if it was created with this user and has no other users)
company = Company.query.get(user.company_id)
if company:
other_users = User.query.filter(
and_(
User.company_id == user.company_id,
User.id != user.id
)
).count()
# Delete the user
db.session.delete(user)
deleted_count += 1
# If no other users, delete the company too
if other_users == 0:
print(f" Also deleting empty company: {company.name} (ID: {company.id})")
db.session.delete(company)
else:
# Delete the user even if company doesn't exist
db.session.delete(user)
deleted_count += 1
if not dry_run and deleted_count > 0:
db.session.commit()
print(f"\nSuccessfully deleted {deleted_count} unverified account(s)")
elif dry_run:
print(f"\nDry run: Would delete {deleted_count} unverified account(s)")
else:
print("No unverified accounts older than 24 hours found")
return deleted_count
def main():
"""Main function to handle command line arguments"""
import argparse
parser = argparse.ArgumentParser(description='Cleanup unverified user accounts older than 24 hours')
parser.add_argument('--dry-run', action='store_true',
help='Show what would be deleted without actually deleting')
parser.add_argument('--quiet', action='store_true',
help='Suppress output except for errors')
args = parser.parse_args()
if not args.quiet:
print(f"Starting cleanup of unverified accounts at {datetime.utcnow()}")
print("-" * 60)
try:
deleted_count = cleanup_unverified_accounts(dry_run=args.dry_run)
if not args.quiet:
print("-" * 60)
print(f"Cleanup completed at {datetime.utcnow()}")
# Exit with 0 for success
sys.exit(0)
except Exception as e:
print(f"Error during cleanup: {str(e)}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -43,6 +43,9 @@ services:
- MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD}
- MAIL_DEFAULT_SENDER=${MAIL_DEFAULT_SENDER}
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY}
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY}
- RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED}
depends_on:
db:
condition: service_healthy

9
docker-cron Normal file
View File

@@ -0,0 +1,9 @@
# Cron job for cleaning up unverified accounts
# Run every hour at minute 0
0 * * * * cd /app && /usr/local/bin/python /app/cleanup_unverified_accounts.py --quiet >> /var/log/cron.log 2>&1
# Cron job for cleaning up inactive verified accounts (no logins, no time entries)
# Run monthly on the 1st at 2:00 AM, cleaning accounts older than 90 days
0 2 1 * * cd /app && /usr/local/bin/python /app/cleanup_inactive_accounts.py --min-age 90 --quiet >> /var/log/cron.log 2>&1
# Empty line required at the end

110
recaptcha_helper.py Normal file
View File

@@ -0,0 +1,110 @@
"""
reCAPTCHA Helper Module
Provides verification functionality for Google reCAPTCHA v2
"""
import os
import requests
import logging
logger = logging.getLogger(__name__)
class ReCaptcha:
"""Helper class for reCAPTCHA verification"""
def __init__(self, app=None):
self.site_key = None
self.secret_key = None
self.enabled = False
if app is not None:
self.init_app(app)
def init_app(self, app):
"""Initialize reCAPTCHA with Flask app configuration"""
self.site_key = os.getenv('RECAPTCHA_SITE_KEY', '')
self.secret_key = os.getenv('RECAPTCHA_SECRET_KEY', '')
self.enabled = os.getenv('RECAPTCHA_ENABLED', 'true').lower() == 'true'
# Store in app config for template access
app.config['RECAPTCHA_SITE_KEY'] = self.site_key
app.config['RECAPTCHA_ENABLED'] = self.enabled
if self.enabled and (not self.site_key or not self.secret_key):
logger.warning("reCAPTCHA is enabled but keys are not configured properly")
def verify(self, response_token, remote_ip=None):
"""
Verify a reCAPTCHA response token
Args:
response_token (str): The g-recaptcha-response from the form
remote_ip (str, optional): The user's IP address
Returns:
tuple: (success: bool, error_message: str or None)
"""
# If reCAPTCHA is disabled, always return success
if not self.enabled:
return True, None
# Check if we have the required configuration
if not self.secret_key:
logger.error("reCAPTCHA secret key is not configured")
return False, "reCAPTCHA is not properly configured"
# Check if response token was provided
if not response_token:
return False, "Please complete the reCAPTCHA challenge"
# Prepare the verification request
verify_url = 'https://www.google.com/recaptcha/api/siteverify'
payload = {
'secret': self.secret_key,
'response': response_token
}
if remote_ip:
payload['remoteip'] = remote_ip
try:
# Make the verification request
response = requests.post(verify_url, data=payload, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('success'):
logger.info("reCAPTCHA verification successful")
return True, None
else:
error_codes = result.get('error-codes', [])
logger.warning(f"reCAPTCHA verification failed: {error_codes}")
# Map error codes to user-friendly messages
error_messages = {
'missing-input-secret': 'reCAPTCHA configuration error',
'invalid-input-secret': 'reCAPTCHA configuration error',
'missing-input-response': 'Please complete the reCAPTCHA challenge',
'invalid-input-response': 'reCAPTCHA verification failed. Please try again.',
'bad-request': 'reCAPTCHA verification failed. Please try again.',
'timeout-or-duplicate': 'reCAPTCHA expired. Please try again.'
}
# Get the first error message or use a default
error_code = error_codes[0] if error_codes else 'unknown'
error_message = error_messages.get(error_code, 'reCAPTCHA verification failed. Please try again.')
return False, error_message
except requests.RequestException as e:
logger.error(f"reCAPTCHA verification request failed: {str(e)}")
return False, "Unable to verify reCAPTCHA. Please try again later."
except Exception as e:
logger.error(f"Unexpected error during reCAPTCHA verification: {str(e)}")
return False, "An error occurred during verification. Please try again."
# Create a singleton instance
recaptcha = ReCaptcha()

View File

@@ -17,3 +17,4 @@ Flask-Migrate==3.1.0
psycopg2-binary==2.9.9
markdown==3.4.4
PyYAML==6.0.1
requests==2.31.0

View File

@@ -73,6 +73,16 @@ if [ -f "migrations_old/run_postgres_migrations.py" ]; then
echo "Found old migration system. Consider removing after confirming Flask-Migrate is working."
fi
# Start cron service for scheduled tasks
echo ""
echo "=== Starting Cron Service ==="
service cron start
if [ $? -eq 0 ]; then
echo "✅ Cron service started for scheduled cleanup tasks"
else
echo "⚠️ Failed to start cron service, cleanup tasks won't run automatically"
fi
# Start the Flask application with gunicorn
echo ""
echo "=== Starting Application ==="

View File

@@ -7,6 +7,9 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
{% if config.RECAPTCHA_ENABLED %}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{% endif %}
<style>
.registration-type {
display: flex;
@@ -202,6 +205,12 @@
</label>
</div>
{% if config.RECAPTCHA_ENABLED %}
<div class="form-group" style="margin-top: 1.5rem;">
<div class="g-recaptcha" data-sitekey="{{ config.RECAPTCHA_SITE_KEY }}"></div>
</div>
{% endif %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Create Account</button>
</div>

View File

@@ -7,6 +7,9 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
{% if config.RECAPTCHA_ENABLED %}
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
{% endif %}
</head>
<body class="auth-page">
<div class="auth-container">
@@ -79,6 +82,12 @@
</label>
</div>
{% if config.RECAPTCHA_ENABLED %}
<div class="form-group" style="margin-top: 1.5rem;">
<div class="g-recaptcha" data-sitekey="{{ config.RECAPTCHA_SITE_KEY }}"></div>
</div>
{% endif %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Create My Workspace</button>
</div>