diff --git a/.env.example b/.env.example index 5f4b6a8..8f7f8d6 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,10 @@ MAIL_PORT=587 MAIL_USE_TLS=true MAIL_USERNAME=your-email@example.com MAIL_PASSWORD=your-password -MAIL_DEFAULT_SENDER=TimeTrack \ No newline at end of file +MAIL_DEFAULT_SENDER=TimeTrack + +# 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 \ No newline at end of file diff --git a/app.py b/app.py index 3543a7c..6e4bdfe 100644 --- a/app.py +++ b/app.py @@ -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')) diff --git a/recaptcha_helper.py b/recaptcha_helper.py new file mode 100644 index 0000000..68c0fed --- /dev/null +++ b/recaptcha_helper.py @@ -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() diff --git a/requirements.txt b/requirements.txt index e8e7320..6f6193a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/templates/register.html b/templates/register.html index 06a5069..c87ed9b 100644 --- a/templates/register.html +++ b/templates/register.html @@ -7,6 +7,9 @@ + {% if config.RECAPTCHA_ENABLED %} + + {% endif %}