from typing import List, Optional from pydantic import BaseModel, EmailStr from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Depends from fastapi.responses import RedirectResponse from minio import Minio from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession import os import uuid from config import Config from database.database import get_db, init_db, User, Profile as DBProfile from routers import profile, vacancies, chat MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") MINIO_BUCKET = os.getenv("MINIO_BUCKET", "resumes") minio_client = None def get_minio_client(): global minio_client if minio_client is None: try: minio_client = Minio( MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=False ) if not minio_client.bucket_exists(MINIO_BUCKET): minio_client.make_bucket(MINIO_BUCKET) print(f"MinIO connected successfully to {MINIO_ENDPOINT}") except Exception as e: print(f"Warning: Could not connect to MinIO at {MINIO_ENDPOINT}: {e}") print("MinIO features will be disabled. Start MinIO to enable file storage.") minio_client = False return minio_client if minio_client else None class Profile(BaseModel): name: str email: EmailStr position: str competencies: Optional[str] = None experience: Optional[str] = None skills: Optional[str] = None country: Optional[str] = None languages: Optional[str] = None employment_format: Optional[str] = None rate: Optional[str] = None relocation: Optional[str] = None cv_url: Optional[str] = None class Vacancy(BaseModel): title: str salary_range: str employment_format: str skills: str link: str vacancies_db = [ Vacancy(title="Python Developer", salary_range="2000-4000 USD", employment_format="remote", skills="Python, FastAPI", link="https://example.com/vacancy/1"), Vacancy(title="Frontend Engineer", salary_range="1500-3000 USD", employment_format="office", skills="React, TypeScript", link="https://example.com/vacancy/2") ] app = FastAPI() app.include_router(profile.router) app.include_router(vacancies.router) app.include_router(chat.router) @app.on_event("startup") async def startup_event(): await init_db() print("FastAPI application started successfully") @app.post("/profile") async def save_profile( name: str = Form(...), email: EmailStr = Form(...), position: str = Form(...), competencies: Optional[str] = Form(None), experience: Optional[str] = Form(None), skills: Optional[str] = Form(None), country: Optional[str] = Form(None), languages: Optional[str] = Form(None), employment_format: Optional[str] = Form(None), rate: Optional[str] = Form(None), relocation: Optional[str] = Form(None), cv: UploadFile = File(...), db: AsyncSession = Depends(get_db) ): if cv.content_type != "application/pdf": raise HTTPException(status_code=400, detail="CV must be a PDF file") client = get_minio_client() if not client: raise HTTPException(status_code=503, detail="File storage (MinIO) is not available. Please start MinIO service.") file_id = f"{email.replace('@','_')}_{cv.filename}" client.put_object( MINIO_BUCKET, file_id, cv.file, length=-1, part_size=10*1024*1024, content_type="application/pdf" ) cv_url = f"/profile/cv/{file_id}" result = await db.execute(select(DBProfile).where(DBProfile.email == email)) existing_profile = result.scalar_one_or_none() if existing_profile: existing_profile.name = name existing_profile.position = position existing_profile.competencies = competencies existing_profile.experience = experience existing_profile.skills = skills existing_profile.country = country existing_profile.languages = languages existing_profile.employment_format = employment_format existing_profile.rate = rate existing_profile.relocation = relocation existing_profile.cv_url = cv_url else: new_profile = DBProfile( name=name, email=email, position=position, competencies=competencies, experience=experience, skills=skills, country=country, languages=languages, employment_format=employment_format, rate=rate, relocation=relocation, cv_url=cv_url ) db.add(new_profile) await db.commit() return {"message": "Profile saved", "cv_url": cv_url} @app.get("/profile/{email}", response_model=Profile) async def get_profile(email: str, db: AsyncSession = Depends(get_db)): result = await db.execute(select(DBProfile).where(DBProfile.email == email)) profile = result.scalar_one_or_none() if not profile: raise HTTPException(status_code=404, detail="Profile not found") return Profile( name=profile.name, email=profile.email, position=profile.position, competencies=profile.competencies, experience=profile.experience, skills=profile.skills, country=profile.country, languages=profile.languages, employment_format=profile.employment_format, rate=profile.rate, relocation=profile.relocation, cv_url=profile.cv_url ) @app.get("/database/tokens") async def get_tokens(db: AsyncSession = Depends(get_db)): result = await db.execute(select(User)) users = result.scalars().all() return [{"token": user.token, "status": user.status, "username": user.username} for user in users] @app.get("/profile/cv/{file_id}") async def download_cv(file_id: str): client = get_minio_client() if not client: raise HTTPException(status_code=503, detail="File storage (MinIO) is not available. Please start MinIO service.") try: response = client.get_object(MINIO_BUCKET, file_id) return RedirectResponse(response.geturl()) except Exception: raise HTTPException(status_code=404, detail="CV not found") @app.get("/vacancies", response_model=List[Vacancy]) async def get_vacancies(): return vacancies_db @app.get("/login") async def login_page(db: AsyncSession = Depends(get_db)): auth_token = str(uuid.uuid4()) bot_name = Config.BOT_NAME new_user = User( telegram_id=None, token=auth_token, username=None, status="pending" ) db.add(new_user) await db.commit() bot_url = f"https://t.me/{bot_name}?start={auth_token}" return { "message": "Нажмите на кнопку для регистрации", "url": bot_url, "check_status_url": f"/check-auth/{auth_token}" } @app.get("/check-auth/{token}") async def check_auth(token: str, db: AsyncSession = Depends(get_db)): """Check if user has completed authentication via Telegram bot""" result = await db.execute(select(User).where(User.token == token)) user = result.scalar_one_or_none() if not user: return {"authorized": False, "message": "Invalid token"} if user.status == "success": return { "authorized": True, "user_data": { "telegram_id": user.telegram_id, "username": user.username, "status": user.status } } return {"authorized": False, "message": "Authentication pending"}