import os from dotenv import load_dotenv import json # Load environment variables FIRST load_dotenv() from flask import Flask, render_template, redirect, url_for, flash, session, request, jsonify, send_file from flask_login import LoginManager, login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash import requests import uuid from urllib.parse import urlencode import re from datetime import datetime from sqlalchemy.orm import joinedload # Import models and database from models import db, User, Introduction, ProfileSummary, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder app = Flask(__name__) # Configure Flask app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or os.environ.get('SQLALCHEMY_DATABASE_URI') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Initialize extensions db.init_app(app) # Note: CSRFProtect disabled for now, using simple token validation # Configure Flask-Login login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'signin' login_manager.login_message = 'Please sign in to access this page.' login_manager.login_message_category = 'info' @login_manager.user_loader def load_user(user_id): return db.session.get(User, str(user_id)) # Admin required decorator def admin_required(f): """Decorator to require admin access""" @login_required def decorated_function(*args, **kwargs): if not current_user.is_admin: flash('You do not have permission to access this page.', 'error') return redirect(url_for('profile')) return f(*args, **kwargs) decorated_function.__name__ = f.__name__ return decorated_function # Note: CSRF protection disabled for simplicity in this development version # GitHub OAuth Configuration GITHUB_CLIENT_ID = os.environ.get('GITHUB_CLIENT_ID') GITHUB_CLIENT_SECRET = os.environ.get('GITHUB_CLIENT_SECRET') # Auto-detect the correct callback URL based on environment SPACE_HOST = os.environ.get('SPACE_HOST') if SPACE_HOST: GITHUB_REDIRECT_URI = f'https://{SPACE_HOST}/api/auth/github/callback' else: GITHUB_REDIRECT_URI = os.environ.get('GITHUB_OAUTH_BACKEND_REDIRECT', 'http://127.0.0.1:5000/auth/github/callback') # GitHub OAuth Functions def generate_github_auth_url(): """Generate GitHub OAuth authorization URL""" params = { 'client_id': GITHUB_CLIENT_ID, 'redirect_uri': GITHUB_REDIRECT_URI, 'scope': 'user:email', 'state': str(uuid.uuid4()) # Simple CSRF protection } return f"https://github.com/login/oauth/authorize?{urlencode(params)}" def exchange_code_for_token(code): """Exchange authorization code for access token""" data = { 'client_id': GITHUB_CLIENT_ID, 'client_secret': GITHUB_CLIENT_SECRET, 'code': code, 'redirect_uri': GITHUB_REDIRECT_URI, } headers = { 'Accept': 'application/json' } response = requests.post('https://github.com/login/oauth/access_token', data=data, headers=headers) if response.status_code == 200: result = response.json() return result.get('access_token') return None def get_github_user_info(access_token): """Get user information from GitHub API""" headers = { 'Authorization': f'token {access_token}', 'Accept': 'application/json' } response = requests.get('https://api.github.com/user', headers=headers) if response.status_code == 200: return response.json() return None def get_github_user_email(access_token): """Get user's primary email from GitHub API""" headers = { 'Authorization': f'token {access_token}', 'Accept': 'application/json' } response = requests.get('https://api.github.com/user/emails', headers=headers) if response.status_code == 200: emails = response.json() # Find primary email for email in emails: if email.get('primary') and email.get('verified'): return email.get('email') # If no primary verified email, use the first one if emails: return emails[0].get('email') return None # Email validation function def validate_email_format(email): """Validate email format with flexible rules""" import re basic_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if re.match(basic_pattern, email): return None return "Invalid email format" # Routes @app.route('/') def index(): if current_user.is_authenticated: return redirect(url_for('profile')) return redirect(url_for('signin')) @app.route('/signin', methods=['GET', 'POST']) def signin(): if current_user.is_authenticated: return redirect(url_for('profile')) if request.method == 'POST': email = request.form.get('email', '').strip() password = request.form.get('password', '') if not email or not password: flash('Please enter both email and password.', 'error') return render_template('signin.html') user = User.query.filter_by(email=email).first() if user and user.check_password(password): # Check if user should be admin admin_email = os.environ.get('ADMIN_EMAIL') if admin_email and email.lower() == admin_email.lower(): if not user.is_admin: user.is_admin = True user.role = 'Admin' db.session.commit() flash('Admin access granted!', 'success') login_user(user) next_page = request.args.get('next') return redirect(next_page or url_for('profile')) else: flash('Invalid email or password.', 'error') return render_template('signin.html') @app.route('/signup', methods=['GET', 'POST']) def signup(): if current_user.is_authenticated: return redirect(url_for('profile')) if request.method == 'POST': name = request.form.get('name', '').strip() email = request.form.get('email', '').strip() password = request.form.get('password', '') password_confirm = request.form.get('password_confirm', '') # Validation errors = [] if not name: errors.append('Name is required') if not email: errors.append('Email is required') else: email_error = validate_email_format(email) if email_error: errors.append(email_error) if not password: errors.append('Password is required') elif len(password) < 8: errors.append('Password must be at least 8 characters long') if not password_confirm: errors.append('Please confirm your password') elif password != password_confirm: errors.append('Passwords do not match') # Check if email already exists if email and User.query.filter_by(email=email).first(): errors.append('Email already registered') if errors: for error in errors: flash(error, 'error') return render_template('signup.html') # Create new user new_user = User( id=uuid.uuid4(), name=name, email=email ) new_user.set_password(password) # Check if user should be admin admin_email = os.environ.get('ADMIN_EMAIL') if admin_email and email.lower() == admin_email.lower(): new_user.is_admin = True new_user.role = 'Admin' try: db.session.add(new_user) db.session.commit() if new_user.is_admin: flash('Admin account created successfully! Please sign in.', 'success') else: flash('Account created successfully! Please sign in.', 'success') return redirect(url_for('signin')) except Exception as e: db.session.rollback() flash('An error occurred while creating your account. Please try again.', 'error') return render_template('signup.html') @app.route('/logout') @login_required def logout(): logout_user() flash('You have been logged out successfully.', 'success') return redirect(url_for('signin')) @app.route('/profile') @login_required def profile(): # Check if user has a profile intro = Introduction.query.filter_by(user_id=current_user.id).first() has_profile = intro is not None if has_profile: # Get all profile data summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() # Get section order if exists section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() section_order = section_order_obj.section_order if section_order_obj else [ 'introduction', 'profile_summary', 'work_experience', 'projects', 'education', 'skills', 'achievements' ] return render_template('profile.html', has_profile=True, intro=intro, summary=summary, work_experiences=work_experiences, projects=projects, educations=educations, skills=skills, achievements=achievements, section_order=section_order) else: return render_template('profile.html', has_profile=False) @app.route('/forgot-password') def forgot_password(): return render_template('forgot_password.html') # GitHub OAuth Routes @app.route('/auth/github') def github_auth(): """Initiate GitHub OAuth flow""" if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET: flash('GitHub OAuth is not configured. Please check your environment variables.', 'error') return redirect(url_for('signin')) auth_url = generate_github_auth_url() return redirect(auth_url) @app.route('/api/auth/github/callback') def github_callback(): """Handle GitHub OAuth callback""" # Check for errors error = request.args.get('error') if error: flash(f'GitHub authentication failed: {error}', 'error') return redirect(url_for('signin')) # Get authorization code and state code = request.args.get('code') state = request.args.get('state') if not code: flash('Authorization code not received from GitHub.', 'error') return redirect(url_for('signin')) # Exchange code for access token access_token = exchange_code_for_token(code) if not access_token: flash('Failed to exchange authorization code for access token.', 'error') return redirect(url_for('signin')) # Get user information from GitHub github_user = get_github_user_info(access_token) if not github_user: flash('Failed to retrieve user information from GitHub.', 'error') return redirect(url_for('signin')) # Get user email email = get_github_user_email(access_token) if not email: flash('Could not retrieve email from GitHub. Please ensure your email is public and verified.', 'error') return redirect(url_for('signin')) # Check if user exists user = User.query.filter_by(email=email).first() if user: # Existing user - check if they should be admin admin_email = os.environ.get('ADMIN_EMAIL') if admin_email and email.lower() == admin_email.lower(): if not user.is_admin: user.is_admin = True user.role = 'Admin' db.session.commit() flash('Admin access granted!', 'success') login_user(user) flash(f'Welcome back, {user.name}!', 'success') return redirect(url_for('profile')) else: # New user - create account # Generate a random password for GitHub users import secrets random_password = secrets.token_urlsafe(32) new_user = User( id=uuid.uuid4(), name=github_user.get('name', github_user.get('login', 'GitHub User')), email=email ) new_user.set_password(random_password) # Check if user should be admin admin_email = os.environ.get('ADMIN_EMAIL') if admin_email and email.lower() == admin_email.lower(): new_user.is_admin = True new_user.role = 'Admin' try: db.session.add(new_user) db.session.commit() login_user(new_user) if new_user.is_admin: flash(f'Admin account created successfully! Welcome, {new_user.name}!', 'success') else: flash(f'Account created successfully! Welcome, {new_user.name}!', 'success') return redirect(url_for('profile')) except Exception as e: db.session.rollback() flash('Failed to create account. Please try again.', 'error') return redirect(url_for('signin')) # Profile Creation Routes @app.route('/profile/create', methods=['GET', 'POST']) @login_required def create_profile(): """Start profile creation process""" # Check if user already has a profile if current_user.introduction: flash('You already have a profile. View or edit it from your profile page.', 'info') return redirect(url_for('profile')) return render_template('create_profile.html') @app.route('/profile/create/introduction', methods=['GET', 'POST']) @login_required def create_introduction(): """Create introduction section""" form_data = {} form_errors = {} if request.method == 'POST': name = request.form.get('name', '').strip() email = request.form.get('email', '').strip() phone = request.form.get('phone', '').strip() linkedin = request.form.get('linkedin', '').strip() github = request.form.get('github', '').strip() website = request.form.get('website', '').strip() # Store form data form_data = { 'name': name, 'email': email, 'phone': phone, 'linkedin': linkedin, 'github': github, 'website': website } # Validation if not name: form_errors['name'] = ['Name is required'] if not email: form_errors['email'] = ['Email is required'] elif not re.match(r'^[^@]+@[^@]+\.[^@]+$', email): form_errors['email'] = ['Invalid email format'] if not phone: form_errors['phone'] = ['Phone is required'] if form_errors: return render_template('create_introduction.html', form_data=form_data, form_errors=form_errors) # Create introduction introduction = Introduction( user_id=current_user.id, name=name, email=email, phone=phone, linkedin=linkedin or None, github=github or None, website=website or None ) try: db.session.add(introduction) db.session.commit() return redirect(url_for('create_profile_summary')) except Exception as e: db.session.rollback() flash('An error occurred while saving your introduction. Please try again.', 'error') return render_template('create_introduction.html', form_data=form_data, form_errors=form_errors) @app.route('/profile/create/profile-summary', methods=['GET', 'POST']) @login_required def create_profile_summary(): """Create profile summary section""" form_data = {} form_errors = {} if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': summary = request.form.get('summary', '').strip() # Store form data form_data = { 'summary': summary } # Validation if not summary: form_errors['summary'] = ['Profile summary is required'] if form_errors: return render_template('create_profile_summary.html', form_data=form_data, form_errors=form_errors) # Create or update profile summary profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() if not profile_summary: profile_summary = ProfileSummary(user_id=current_user.id) profile_summary.summary = summary profile_summary.ai_generated = False try: db.session.add(profile_summary) db.session.commit() return redirect(url_for('create_work_experience')) except Exception as e: db.session.rollback() flash('An error occurred while saving your profile summary. Please try again.', 'error') # For GET request or if returning from POST with error # Get existing summary if any existing_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() if existing_summary: form_data['summary'] = existing_summary.summary return render_template('create_profile_summary.html', form_data=form_data, form_errors=form_errors) @app.route('/profile/create/generate-summary', methods=['POST']) @login_required def generate_ai_summary(): """Generate AI-powered profile summary using OpenAI""" if not current_user.introduction: return jsonify({'error': 'Please complete your introduction first'}), 400 try: # Get user's introduction and other relevant info intro = current_user.introduction # Get additional profile data if available work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).all() projects = Project.query.filter_by(user_id=current_user.id).all() educations = Education.query.filter_by(user_id=current_user.id).all() skills = Skill.query.filter_by(user_id=current_user.id).all() # Prepare context for AI context = f""" Personal Information: - Name: {intro.name} - Email: {intro.email} - Phone: {intro.phone} - LinkedIn: {intro.linkedin or 'Not provided'} - GitHub: {intro.github or 'Not provided'} - Website: {intro.website or 'Not provided'} """ if work_experiences: context += "\n\nWork Experience:\n" for exp in work_experiences[:3]: # Limit to first 3 experiences context += f"- {exp.title} at {exp.organization} ({exp.start_month}/{exp.start_year} - {'Present' if not exp.end_month else f'{exp.end_month}/{exp.end_year}'})\n" if educations: context += "\nEducation:\n" for edu in educations[:2]: # Limit to first 2 educations context += f"- {edu.title} at {edu.organization} ({edu.start_month}/{edu.start_year} - {'Present' if not edu.end_month else f'{edu.end_month}/{edu.end_year}'})\n" if skills: skill_list = [s.skill for s in skills[:10]] # Limit to first 10 skills context += f"\nSkills: {', '.join(skill_list)}" # Using OpenAI API directly def generate_with_openai(): from openai import OpenAI client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY')) prompt = f""" Create a professional profile summary based on this information: {context} Requirements: - Write in third person - Keep it concise (3-5 sentences) - Highlight professional strengths and achievements - Make it engaging and professional - Focus on what makes this person unique """ response = client.chat.completions.create( model=os.environ.get('OPENAI_MODEL', 'gpt-4o'), messages=[ {"role": "system", "content": "You are an expert resume writer and career coach. Write compelling professional profiles that stand out."}, {"role": "user", "content": prompt} ], max_tokens=250, temperature=0.8 ) return response.choices[0].message.content.strip() # Generate the summary ai_summary = generate_with_openai() # Save the AI-generated summary profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() if not profile_summary: profile_summary = ProfileSummary(user_id=current_user.id) profile_summary.summary = ai_summary profile_summary.ai_generated = True db.session.add(profile_summary) db.session.commit() return jsonify({ 'success': True, 'summary': ai_summary, 'message': 'Profile summary generated successfully!' }) except Exception as e: db.session.rollback() return jsonify({'success': False, 'error': f'Failed to generate summary: {str(e)}'}), 500 @app.route('/profile/create/work-experience', methods=['GET', 'POST']) @login_required def create_work_experience(): """Create work experience section""" if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': # Clear existing work experiences for this user WorkExperience.query.filter_by(user_id=current_user.id).delete() # Get form data organizations = request.form.getlist('organization[]') titles = request.form.getlist('title[]') start_months = request.form.getlist('start_month[]') start_years = request.form.getlist('start_year[]') end_months = request.form.getlist('end_month[]') end_years = request.form.getlist('end_year[]') is_present_list = request.form.getlist('is_present[]') remarks_list = request.form.getlist('remarks[]') # Find the maximum length among all lists to handle inconsistent lengths max_length = max(len(organizations), len(titles), len(start_months), len(start_years), len(end_months), len(end_years), len(remarks_list)) # Save each work experience for i in range(max_length): org = organizations[i] if i < len(organizations) else '' title = titles[i] if i < len(titles) else '' if org.strip(): # Only save if organization is provided work_exp = WorkExperience( user_id=current_user.id, organization=org.strip(), title=title.strip(), start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, order=i ) db.session.add(work_exp) try: db.session.commit() flash('Work experience saved successfully.', 'success') return redirect(url_for('create_projects')) except Exception as e: db.session.rollback() flash('An error occurred while saving work experience.', 'error') # GET request - show form form_data = { 'work_experiences': WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() } return render_template('create_work_experience.html', form_data=form_data, current_year=datetime.now().year) @app.route('/profile/create/projects', methods=['GET', 'POST']) @login_required def create_projects(): """Create projects section""" if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': # Clear existing projects for this user Project.query.filter_by(user_id=current_user.id).delete() # Get form data organizations = request.form.getlist('organization[]') titles = request.form.getlist('title[]') start_months = request.form.getlist('start_month[]') start_years = request.form.getlist('start_year[]') end_months = request.form.getlist('end_month[]') end_years = request.form.getlist('end_year[]') is_present_list = request.form.getlist('is_present[]') remarks_list = request.form.getlist('remarks[]') # Find the maximum length among all lists to handle inconsistent lengths max_length = max(len(organizations), len(titles), len(start_months), len(start_years), len(end_months), len(end_years), len(remarks_list)) # Save each project for i in range(max_length): title = titles[i] if i < len(titles) else '' org = organizations[i] if i < len(organizations) else '' if title.strip(): # Only save if title is provided project = Project( user_id=current_user.id, organization=org.strip() if org else None, title=title.strip(), start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, order=i ) db.session.add(project) try: db.session.commit() flash('Projects saved successfully.', 'success') return redirect(url_for('create_education')) except Exception as e: db.session.rollback() flash('An error occurred while saving projects.', 'error') # GET request - show form form_data = { 'projects': Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() } return render_template('create_projects.html', form_data=form_data, current_year=datetime.now().year) @app.route('/profile/create/education', methods=['GET', 'POST']) @login_required def create_education(): """Create education section""" if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': # Clear existing education for this user Education.query.filter_by(user_id=current_user.id).delete() # Get form data organizations = request.form.getlist('organization[]') titles = request.form.getlist('title[]') start_months = request.form.getlist('start_month[]') start_years = request.form.getlist('start_year[]') end_months = request.form.getlist('end_month[]') end_years = request.form.getlist('end_year[]') is_present_list = request.form.getlist('is_present[]') remarks_list = request.form.getlist('remarks[]') # Find the maximum length among all lists to handle inconsistent lengths max_length = max(len(organizations), len(titles), len(start_months), len(start_years), len(end_months), len(end_years), len(remarks_list)) # Save each education for i in range(max_length): org = organizations[i] if i < len(organizations) else '' title = titles[i] if i < len(titles) else '' if org.strip(): # Only save if organization is provided education = Education( user_id=current_user.id, organization=org.strip(), title=title.strip(), start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None, start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None, end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None, end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None, remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None, order=i ) db.session.add(education) try: db.session.commit() flash('Education saved successfully.', 'success') return redirect(url_for('create_skills')) except Exception as e: db.session.rollback() flash('An error occurred while saving education.', 'error') # GET request - show form form_data = { 'educations': Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() } return render_template('create_education.html', form_data=form_data, current_year=datetime.now().year) @app.route('/profile/create/skills', methods=['GET', 'POST']) @login_required def create_skills(): """Create skills section""" form_data = {} form_errors = {} if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': # Get skills from form skills_text = request.form.get('skills', '').strip() # Store form data form_data = { 'skills': skills_text, 'skills_preview': [skill.strip() for skill in skills_text.split(',') if skill.strip()] if skills_text else [] } # Validation - skills are optional but if provided, they should be valid if skills_text and len(skills_text.split(',')) > 50: form_errors['skills'] = ['You can add up to 50 skills maximum'] if form_errors: return render_template('create_skills.html', form_data=form_data, form_errors=form_errors) # Clear existing skills for this user Skill.query.filter_by(user_id=current_user.id).delete() if skills_text: skills_list = [skill.strip() for skill in skills_text.split(',') if skill.strip()] # Save each skill for i, skill in enumerate(skills_list): new_skill = Skill( user_id=current_user.id, skill=skill, order=i ) db.session.add(new_skill) try: db.session.commit() flash('Skills saved successfully.', 'success') return redirect(url_for('create_achievements')) except Exception as e: db.session.rollback() flash('An error occurred while saving skills.', 'error') # GET request - show form existing_skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() skills_text = ', '.join([skill.skill for skill in existing_skills]) form_data = { 'skills': skills_text, 'skills_preview': [skill.skill for skill in existing_skills] } return render_template('create_skills.html', form_data=form_data, form_errors=form_errors) @app.route('/profile/create/achievements', methods=['GET', 'POST']) @login_required def create_achievements(): """Create achievements section""" form_data = {} form_errors = {} if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': # Get achievements from form achievements_text = request.form.get('achievements', '').strip() # Store form data form_data = { 'achievements': achievements_text, 'achievements_preview': [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()] if achievements_text else [] } # Validation - achievements are optional but if provided, they should be valid if achievements_text and len(achievements_text.split(',')) > 50: form_errors['achievements'] = ['You can add up to 50 achievements maximum'] if form_errors: return render_template('create_achievements.html', form_data=form_data, form_errors=form_errors) # Clear existing achievements for this user Achievement.query.filter_by(user_id=current_user.id).delete() if achievements_text: achievements_list = [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()] # Save each achievement for i, achievement in enumerate(achievements_list): new_achievement = Achievement( user_id=current_user.id, achievement=achievement, order=i ) db.session.add(new_achievement) try: db.session.commit() flash('Achievements saved successfully.', 'success') return redirect(url_for('create_preview')) except Exception as e: db.session.rollback() flash('An error occurred while saving achievements.', 'error') # GET request - show form existing_achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() achievements_text = ', '.join([achievement.achievement for achievement in existing_achievements]) form_data = { 'achievements': achievements_text, 'achievements_preview': [achievement.achievement for achievement in existing_achievements] } return render_template('create_achievements.html', form_data=form_data, form_errors=form_errors) @app.route('/profile/create/preview', methods=['GET', 'POST']) @login_required def create_preview(): """Preview and finalize profile""" if not current_user.introduction: flash('Please complete your introduction first.', 'error') return redirect(url_for('create_introduction')) if request.method == 'POST': action = request.form.get('action') if action == 'submit': # Save section order section_order = request.form.get('section_order', '[]') try: order_data = json.loads(section_order) # Update or create section order profile_order = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() if not profile_order: profile_order = ProfileSectionOrder(user_id=current_user.id) profile_order.section_order = order_data db.session.add(profile_order) db.session.commit() flash('Profile created successfully!', 'success') return redirect(url_for('profile')) except Exception as e: db.session.rollback() flash('An error occurred while saving your profile.', 'error') elif action == 'clear': # Delete all profile data try: # Delete all profile sections if current_user.introduction: db.session.delete(current_user.introduction) if current_user.profile_summary: db.session.delete(current_user.profile_summary) if current_user.section_order: db.session.delete(current_user.section_order) # Delete collections WorkExperience.query.filter_by(user_id=current_user.id).delete() Project.query.filter_by(user_id=current_user.id).delete() Education.query.filter_by(user_id=current_user.id).delete() Skill.query.filter_by(user_id=current_user.id).delete() Achievement.query.filter_by(user_id=current_user.id).delete() db.session.commit() flash('Profile cleared successfully.', 'success') return redirect(url_for('profile')) except Exception as e: db.session.rollback() flash('An error occurred while clearing your profile.', 'error') # Get all profile data intro = current_user.introduction summary = current_user.profile_summary work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() # Get default section order default_order = ['introduction', 'profile_summary', 'work_experience', 'projects', 'education', 'skills', 'achievements'] return render_template('create_preview.html', intro=intro, summary=summary, work_experiences=work_experiences, projects=projects, educations=educations, skills=skills, achievements=achievements, default_order=default_order) @app.route('/ping') def ping(): return {"ok": True, "msg": "pong from app.py"} @app.cli.command() def create_tables(): """Create database tables""" with app.app_context(): db.create_all() print("Database tables created successfully!") # Resume Generation Routes @app.route('/profile/generate-resume/') @login_required def generate_resume(format_type): """Generate resume in specified format""" # Check if user has profile data intro = Introduction.query.filter_by(user_id=current_user.id).first() if not intro: flash('Please create your profile first.', 'error') return redirect(url_for('profile')) # Get all profile data summary = ProfileSummary.query.filter_by(user_id=current_user.id).first() work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all() projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all() educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all() skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all() achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all() # Get section order section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first() section_order = section_order_obj.section_order if section_order_obj else [ 'introduction', 'profile_summary', 'work_experience', 'projects', 'education', 'skills', 'achievements' ] try: if format_type == 'word': return generate_word_resume( intro, summary, work_experiences, projects, educations, skills, achievements, section_order ) elif format_type == 'pdf-standard': return generate_pdf_resume( intro, summary, work_experiences, projects, educations, skills, achievements, section_order, 'standard' ) elif format_type == 'pdf-modern': return generate_pdf_resume( intro, summary, work_experiences, projects, educations, skills, achievements, section_order, 'modern' ) else: flash('Invalid resume format.', 'error') return redirect(url_for('profile')) except Exception as e: import traceback app.logger.error(f"Error generating resume: {str(e)}") app.logger.error(traceback.format_exc()) flash(f'An error occurred while generating your resume: {str(e)}', 'error') return redirect(url_for('profile')) def generate_word_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order): """Generate Word document resume""" try: from docx import Document from docx.shared import Pt, Inches, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH # Create document doc = Document() # Set default font style = doc.styles['Normal'] font = style.font font.name = 'Calibri' font.size = Pt(11) # Header section name_para = doc.add_paragraph() name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER name_run = name_para.add_run(intro.name.upper()) name_run.bold = True name_run.size = Pt(16) # Contact info contact_para = doc.add_paragraph() contact_para.alignment = WD_ALIGN_PARAGRAPH.CENTER contact_para.add_run(f"Email: {intro.email} | Phone: {intro.phone}") if intro.linkedin: contact_para.add_run(" | LinkedIn") if intro.github: contact_para.add_run(" | GitHub") if intro.website: contact_para.add_run(" | Website") doc.add_paragraph() # Profile summary if summary: doc.add_heading('Professional Summary', level=1) summary_para = doc.add_paragraph(summary.summary) if summary.ai_generated: ai_para = doc.add_paragraph("AI Generated") ai_para.italic = True ai_para.runs[0].font.size = Pt(9) doc.add_paragraph() for section_name in section_order: if section_name == 'work_experience' and work_experiences: doc.add_heading('Work Experience', level=1) for exp in work_experiences: # Title and organization exp_para = doc.add_paragraph() exp_para.add_run(exp.title).bold = True exp_para.add_run(f" at {exp.organization}").italic = True # Date date_para = doc.add_paragraph() date_text = f"{exp.start_month}/{exp.start_year} - " if exp.end_year: date_text += f"{exp.end_month}/{exp.end_year}" else: date_text += "Present" date_para.add_run(date_text) # Remarks if exp.remarks: remarks_para = doc.add_paragraph(exp.remarks) doc.add_paragraph() elif section_name == 'projects' and projects: doc.add_heading('Projects', level=1) for project in projects: # Title and organization proj_para = doc.add_paragraph() proj_para.add_run(project.title).bold = True if project.organization: proj_para.add_run(f" at {project.organization}").italic = True # Date date_para = doc.add_paragraph() date_text = f"{project.start_month}/{project.start_year} - " if project.end_year: date_text += f"{project.end_month}/{project.end_year}" else: date_text += "Present" date_para.add_run(date_text) # Remarks if project.remarks: remarks_para = doc.add_paragraph(project.remarks) doc.add_paragraph() elif section_name == 'education' and educations: doc.add_heading('Education', level=1) for edu in educations: # Title and organization edu_para = doc.add_paragraph() edu_para.add_run(edu.title).bold = True edu_para.add_run(f" at {edu.organization}").italic = True # Date date_para = doc.add_paragraph() date_text = f"{edu.start_month}/{edu.start_year} - " if edu.end_year: date_text += f"{edu.end_month}/{edu.end_year}" else: date_text += "Present" date_para.add_run(date_text) # Remarks if edu.remarks: remarks_para = doc.add_paragraph(edu.remarks) doc.add_paragraph() elif section_name == 'skills' and skills: doc.add_heading('Skills', level=1) skills_text = ", ".join([skill.skill for skill in skills]) doc.add_paragraph(skills_text) doc.add_paragraph() elif section_name == 'achievements' and achievements: doc.add_heading('Achievements', level=1) for achievement in achievements: doc.add_paragraph(f"• {achievement.achievement}") doc.add_paragraph() # Save to bytes from io import BytesIO doc_buffer = BytesIO() doc.save(doc_buffer) doc_buffer.seek(0) # Return as downloadable file username = intro.name.replace(' ', '_') return send_file( doc_buffer, mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document', as_attachment=True, download_name=f'{username}_resume.docx' ) except Exception as e: app.logger.error(f"Error generating Word resume: {str(e)}") raise def generate_pdf_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order, template_type): """Generate PDF resume using ReportLab""" try: from pdf_generator import create_pdf_resume from io import BytesIO import calendar def format_date(month, year): """Format month and year as 'Month Year'""" if month and year: try: month_name = calendar.month_name[int(month)] return f"{month_name[:3]} {year}" except: return f"{month}/{year}" return "" # Prepare data for PDF generation work_exp_list = [] if work_experiences: for exp in work_experiences: start_date = format_date(exp.start_month, exp.start_year) end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year) work_exp_list.append({ 'title': exp.title, 'organization': exp.organization, 'start_date': start_date, 'end_date': end_date, 'remarks': exp.remarks or '' }) projects_list = [] if projects: for proj in projects: start_date = format_date(proj.start_month, proj.start_year) end_date = "Present" if not proj.end_month or not proj.end_year else format_date(proj.end_month, proj.end_year) projects_list.append({ 'title': proj.title, 'organization': proj.organization, 'start_date': start_date, 'end_date': end_date, 'remarks': proj.remarks or '' }) education_list = [] if educations: for edu in educations: start_date = format_date(edu.start_month, edu.start_year) end_date = "Present" if not edu.end_month or not edu.end_year else format_date(edu.end_month, edu.end_year) education_list.append({ 'title': edu.title, 'organization': edu.organization, 'start_date': start_date, 'end_date': end_date, 'remarks': edu.remarks or '' }) # Convert skills and achievements to comma-separated strings skills_text = ', '.join([skill.skill for skill in skills]) if skills else '' achievements_text = ', '.join([achievement.achievement for achievement in achievements]) if achievements else '' # Create data dictionary data = { 'name': intro.name, 'email': intro.email, 'phone': intro.phone, 'linkedin': intro.linkedin, 'github': intro.github, 'website': intro.website, 'summary': summary.summary if summary else '', 'work_experience': work_exp_list, 'projects': projects_list, 'education': education_list, 'skills': skills_text, 'achievements': achievements_text, 'sections_order': section_order } # Generate PDF pdf_bytes = create_pdf_resume(data, template_type) if pdf_bytes: # Return as downloadable file username = intro.name.replace(' ', '_') filename = f'{username}_resume.pdf' if template_type == 'standard' else f'{username}_resume_modern.pdf' pdf_buffer = BytesIO(pdf_bytes) return send_file( pdf_buffer, mimetype='application/pdf', as_attachment=True, download_name=filename ) else: raise Exception("PDF generation failed") except Exception as e: app.logger.error(f"Error generating PDF resume: {str(e)}") raise # Admin Panel Route @app.route('/admin') @admin_required def admin_panel(): """Admin panel to manage users""" users = User.query.order_by(User.created_at.desc()).all() return render_template('admin.html', users=users) # Admin API Endpoints @app.route('/api/admin/users', methods=['GET']) @admin_required def api_get_users(): """Get list of all users""" try: users = User.query.order_by(User.created_at.desc()).all() users_data = [] for user in users: users_data.append({ 'id': str(user.id), 'email': user.email, 'name': user.name, 'created_at': user.created_at.isoformat() if user.created_at else None, 'is_admin': user.is_admin, 'role': user.role }) return jsonify({ 'success': True, 'users': users_data }) except Exception as e: app.logger.error(f"Error fetching users: {str(e)}") return jsonify({ 'success': False, 'error': str(e) }), 500 @app.route('/api/admin/users/', methods=['DELETE']) @admin_required def api_delete_user(user_id): """Delete a user and all their profile data""" try: # Find the user and store email before deletion user = User.query.get(user_id) if not user: return jsonify({ 'success': False, 'error': 'User not found' }), 404 # Store user email for success message user_email = user.email # Don't allow deleting admin users if user.is_admin: return jsonify({ 'success': False, 'error': 'Cannot delete admin users' }), 400 # Don't allow deleting self if str(user.id) == str(current_user.id): return jsonify({ 'success': False, 'error': 'Cannot delete your own account' }), 400 # Use direct SQL to delete all profile data first # This ensures we handle any duplicate data that might exist # Delete from all profile tables from sqlalchemy import text # Delete introductions (handle potential duplicates) db.session.execute(text("DELETE FROM introductions WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete profile summaries db.session.execute(text("DELETE FROM profile_summaries WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete work experiences db.session.execute(text("DELETE FROM work_experiences WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete projects db.session.execute(text("DELETE FROM projects WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete education db.session.execute(text("DELETE FROM educations WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete skills db.session.execute(text("DELETE FROM skills WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete achievements db.session.execute(text("DELETE FROM achievements WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Delete section order db.session.execute(text("DELETE FROM profile_section_orders WHERE user_id = :user_id"), {'user_id': str(user_id)}) # Finally delete the user db.session.execute(text("DELETE FROM users WHERE id = :user_id"), {'user_id': str(user_id)}) # Expunge the user object from session to avoid the deleted instance warning db.session.expunge(user) db.session.commit() return jsonify({ 'success': True, 'message': f'User {user_email} and all their profile data have been deleted successfully' }) except Exception as e: db.session.rollback() app.logger.error(f"Error deleting user: {str(e)}") return jsonify({ 'success': False, 'error': str(e) }), 500 def create_tables_if_needed(): """Create database tables if they don't exist""" with app.app_context(): # Check if tables exist inspector = db.inspect(db.engine) table_names = inspector.get_table_names() if not table_names or 'users' not in table_names: print("Creating database tables...") db.create_all() print("Database tables created successfully!") # Run admin migration if needed try: from sqlalchemy import text columns = [column['name'] for column in inspector.get_columns('users')] if 'is_admin' not in columns: print("Running admin migration...") db.session.execute(text('ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE')) db.session.execute(text('ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT \'User\'')) db.session.commit() print("Admin migration completed!") except Exception as e: print(f"Migration error: {e}") # Initialize database only when running directly, not when imported if __name__ == "__main__": create_tables_if_needed() port = int(os.environ.get('PORT', 5000)) # Railway requires binding to 0.0.0.0 # Check if we're running on Railway is_railway = os.environ.get('RAILWAY_ENVIRONMENT') or os.environ.get('RAILWAY_ENVIRONMENT_NAME') host = '0.0.0.0' if is_railway else '127.0.0.1' print(f"Flask running at http://{host}:{port}") # Disable debug mode in production debug = not is_railway app.run(host=host, port=port, debug=debug)