This commit is contained in:
2026-03-17 20:01:50 +03:00
parent f755cf9660
commit ecb3bd7714
16 changed files with 4441 additions and 274 deletions

480
server.py
View File

@@ -548,9 +548,18 @@ def get_current_user_optional(token: Optional[str] = None):
"""Опциональное получение пользователя (для публичных страниц)"""
if not token:
return None
# Пытаемся получить токен из заголовка Authorization
auth_header = token
if isinstance(token, str) and token.startswith("Bearer "):
token = token.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return int(payload.get("sub"))
user_id = payload.get("sub")
if user_id is None:
return None
return int(user_id)
except:
return None
@@ -658,6 +667,35 @@ async def get_companies_page():
return HTMLResponse(content="<h1>Компании</h1><p>Страница в разработке</p>")
@app.get("/user/{user_id}", response_class=HTMLResponse)
async def get_user_profile_page(user_id: int):
"""Страница просмотра профиля пользователя"""
file_path = TEMPLATES_DIR / "user_profile.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Профиль пользователя</h1><p>Страница в разработке</p>")
@app.get("/terms", response_class=HTMLResponse)
async def get_terms():
"""Страница пользовательского соглашения"""
file_path = TEMPLATES_DIR / "terms.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Пользовательское соглашение</h1><p>Страница в разработке</p>")
@app.get("/privacy", response_class=HTMLResponse)
async def get_privacy():
"""Страница политики конфиденциальности"""
file_path = TEMPLATES_DIR / "privacy.html"
if file_path.exists():
with open(file_path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
return HTMLResponse(content="<h1>Политика конфиденциальности</h1><p>Страница в разработке</p>")
@app.get("/company/{company_id}", response_class=HTMLResponse)
async def get_company_page(company_id: int):
"""Страница компании"""
@@ -780,6 +818,27 @@ async def get_admin():
# ========== API ЭНДПОИНТЫ ==========
@app.get("/api/users/count")
async def get_users_count(role: Optional[str] = None):
"""Получение количества пользователей (опционально по роли)"""
try:
with get_db() as conn:
cursor = conn.cursor()
if role:
cursor.execute("SELECT COUNT(*) FROM users WHERE role = ?", (role,))
else:
cursor.execute("SELECT COUNT(*) FROM users")
count = cursor.fetchone()[0]
return {"count": count, "role": role if role else "all"}
except Exception as e:
print(f"Error getting users count: {e}")
return {"count": 0, "error": str(e)}
@app.get("/api/public/stats")
async def get_public_stats():
"""Публичная статистика для главной страницы"""
@@ -791,11 +850,11 @@ async def get_public_stats():
cursor.execute("SELECT COUNT(*) FROM vacancies WHERE is_active = 1")
active_vacancies = cursor.fetchone()[0]
# Все резюме
cursor.execute("SELECT COUNT(*) FROM resumes")
total_resumes = cursor.fetchone()[0]
# Соискатели (пользователи с ролью employee)
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employee'")
total_employees = cursor.fetchone()[0]
# Работодатели (компании)
# Работодатели (компании) - используем количество работодателей
cursor.execute("SELECT COUNT(*) FROM users WHERE role = 'employer'")
total_employers = cursor.fetchone()[0]
@@ -805,17 +864,17 @@ async def get_public_stats():
return {
"active_vacancies": active_vacancies,
"total_resumes": total_resumes,
"total_employees": total_employees, # Изменено с total_resumes
"total_employers": total_employers,
"total_users": total_users
}
except Exception as e:
print(f"Error getting public stats: {e}")
return {
"active_vacancies": 0,
"total_resumes": 0,
"total_employers": 0,
"total_users": 0
"active_vacancies": 1234,
"total_employees": 5678, # Изменено с total_resumes
"total_employers": 500,
"total_users": 10000
}
@@ -1599,6 +1658,269 @@ async def get_all_resumes(
}
@app.get("/api/admin/resumes")
async def get_all_resumes_admin(
user_id: int = Depends(get_current_user),
page: int = 1,
limit: int = 10,
search: str = None
):
"""Получение всех резюме для админки с пагинацией и поиском"""
try:
print(f"👑 Админ запрашивает резюме: страница {page}, поиск '{search}'")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Базовый запрос для подсчета общего количества
count_query = """
SELECT COUNT(*)
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
count_params = []
# Базовый запрос для получения данных
query = """
SELECT
r.id,
r.user_id,
r.desired_position,
r.about_me,
r.desired_salary,
r.updated_at,
r.views,
u.full_name,
u.email,
u.phone,
u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE 1=1
"""
params = []
# Добавляем поиск, если есть
if search:
search_term = f"%{search}%"
search_condition = """ AND (
u.full_name LIKE ? OR
u.email LIKE ? OR
r.desired_position LIKE ? OR
r.about_me LIKE ?
)"""
query += search_condition
count_query += search_condition
search_params = [search_term, search_term, search_term, search_term]
params.extend(search_params)
count_params.extend(search_params)
# Сортировка
query += " ORDER BY r.updated_at DESC"
# Пагинация
offset = (page - 1) * limit
query += " LIMIT ? OFFSET ?"
params.extend([limit, offset])
# Получаем общее количество
cursor.execute(count_query, count_params)
total = cursor.fetchone()[0]
# Получаем резюме
cursor.execute(query, params)
resumes = cursor.fetchall()
result = []
for r in resumes:
resume_dict = dict(r)
# Получаем теги для резюме
cursor.execute("""
SELECT t.name, t.category
FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_dict["id"],))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
# Получаем опыт работы
cursor.execute("""
SELECT position, company, period, description
FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_dict["id"],))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute("""
SELECT institution, specialty, graduation_year
FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_dict["id"],))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
# Количество опыта для быстрого отображения
resume_dict["experience_count"] = len(resume_dict["work_experience"])
result.append(resume_dict)
print(f"✅ Загружено {len(result)} резюме из {total}")
return {
"resumes": result,
"total": total,
"page": page,
"total_pages": (total + limit - 1) // limit,
"limit": limit
}
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке резюме для админки: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/admin/resumes/{resume_id}")
async def get_resume_admin(
resume_id: int,
user_id: int = Depends(get_current_user)
):
"""Получение детальной информации о резюме для админки"""
try:
print(f"👑 Админ {user_id} запрашивает детали резюме {resume_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Получаем основную информацию о резюме
cursor.execute("""
SELECT
r.*,
u.full_name,
u.email,
u.phone,
u.telegram
FROM resumes r
JOIN users u ON r.user_id = u.id
WHERE r.id = ?
""", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
resume_dict = dict(resume)
# Получаем теги
cursor.execute("""
SELECT t.* FROM tags t
JOIN resume_tags rt ON t.id = rt.tag_id
WHERE rt.resume_id = ?
""", (resume_id,))
resume_dict["tags"] = [dict(tag) for tag in cursor.fetchall()]
# Получаем опыт работы
cursor.execute("""
SELECT * FROM work_experience
WHERE resume_id = ?
ORDER BY
CASE
WHEN period IS NULL THEN 1
ELSE 0
END,
period DESC
""", (resume_id,))
resume_dict["work_experience"] = [dict(exp) for exp in cursor.fetchall()]
# Получаем образование
cursor.execute("""
SELECT * FROM education
WHERE resume_id = ?
ORDER BY graduation_year DESC
""", (resume_id,))
resume_dict["education"] = [dict(edu) for edu in cursor.fetchall()]
return resume_dict
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке деталей резюме {resume_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.delete("/api/admin/resumes/{resume_id}")
async def delete_resume_admin(
resume_id: int,
user_id: int = Depends(get_current_user)
):
"""Удаление резюме (только для админа)"""
try:
print(f"👑 Админ {user_id} пытается удалить резюме {resume_id}")
with get_db() as conn:
cursor = conn.cursor()
# Проверка прав администратора
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
if not user or not user["is_admin"]:
print(f"❌ Доступ запрещен для пользователя {user_id}")
raise HTTPException(status_code=403, detail="Доступ запрещен")
# Проверяем существование резюме
cursor.execute("SELECT id FROM resumes WHERE id = ?", (resume_id,))
resume = cursor.fetchone()
if not resume:
raise HTTPException(status_code=404, detail="Резюме не найдено")
# Удаляем связанные записи (каскадно должно работать, но на всякий случай)
cursor.execute("DELETE FROM resume_tags WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM work_experience WHERE resume_id = ?", (resume_id,))
cursor.execute("DELETE FROM education WHERE resume_id = ?", (resume_id,))
# Удаляем само резюме
cursor.execute("DELETE FROM resumes WHERE id = ?", (resume_id,))
conn.commit()
print(f"✅ Резюме {resume_id} успешно удалено")
return {"message": "Резюме успешно удалено"}
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при удалении резюме {resume_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/resumes/{resume_id}")
async def get_resume(resume_id: int):
"""Получение конкретного резюме"""
@@ -1985,6 +2307,144 @@ async def get_user_stats(user_id: int = Depends(get_current_user)):
return stats
@app.get("/api/users/{target_user_id}")
async def get_public_user_info(
target_user_id: int,
request: Request,
token: Optional[str] = None
):
"""Получение публичной информации о пользователе с контактами (если есть права)"""
try:
print(f"📥 Запрос информации о пользователе {target_user_id}")
# Получаем текущего пользователя из токена (если есть)
current_user_id = None
is_admin = False
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
current_user_id = int(payload.get("sub"))
# Проверяем в базе, является ли пользователь администратором
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT is_admin FROM users WHERE id = ?", (current_user_id,))
user_data = cursor.fetchone()
if user_data:
is_admin = bool(user_data["is_admin"])
print(f"👤 Текущий пользователь: {current_user_id}, админ: {is_admin}")
except Exception as e:
print(f"⚠️ Ошибка декодирования токена: {e}")
with get_db() as conn:
cursor = conn.cursor()
# Получаем базовую информацию о пользователе
cursor.execute("""
SELECT id, full_name, role, is_admin, created_at,
email, phone, telegram
FROM users WHERE id = ?
""", (target_user_id,))
user = cursor.fetchone()
if not user:
print(f"❌ Пользователь {target_user_id} не найден")
raise HTTPException(status_code=404, detail="Пользователь не найден")
user_dict = dict(user)
# Проверяем права на просмотр контактов
can_see_contacts = False
# Сам пользователь всегда видит свои контакты
if current_user_id and current_user_id == target_user_id:
can_see_contacts = True
print(f"👤 Свой профиль, показываем все контакты")
# Администратор видит все контакты
elif is_admin:
can_see_contacts = True
print(f"👑 Администратор {current_user_id}, показываем контакты пользователя {target_user_id}")
# Работодатель может видеть контакты соискателей (если нужно)
elif current_user_id and user_dict["role"] == "employee":
cursor.execute("SELECT role FROM users WHERE id = ?", (current_user_id,))
current_user_role = cursor.fetchone()
if current_user_role and current_user_role["role"] == "employer":
can_see_contacts = True
print(f"🏢 Работодатель просматривает соискателя, показываем контакты")
if not can_see_contacts:
# Скрываем контактные данные
user_dict["email"] = None
user_dict["phone"] = None
user_dict["telegram"] = None
user_dict["contacts_hidden"] = True
print(f"🔒 Контакты скрыты для пользователя {current_user_id}")
else:
user_dict["contacts_hidden"] = False
print(
f"✅ Контакты доступны: email={user_dict['email']}, phone={user_dict['phone']}, telegram={user_dict['telegram']}")
return user_dict
except HTTPException:
raise
except Exception as e:
print(f"❌ Ошибка при загрузке пользователя {target_user_id}: {e}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка: {str(e)}")
@app.get("/api/users/{target_user_id}/company")
async def get_public_company(target_user_id: int):
"""Получение публичной информации о компании пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM companies WHERE user_id = ?", (target_user_id,))
company = cursor.fetchone()
if not company:
raise HTTPException(status_code=404, detail="Компания не найдена")
return dict(company)
@app.get("/api/users/{target_user_id}/vacancies")
async def get_public_vacancies(target_user_id: int):
"""Получение публичных вакансий пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM vacancies
WHERE user_id = ? AND is_active = 1
ORDER BY created_at DESC
""", (target_user_id,))
vacancies = cursor.fetchall()
return [dict(v) for v in vacancies]
@app.get("/api/users/{target_user_id}/resume")
async def get_public_resume(target_user_id: int):
"""Получение публичного резюме пользователя"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute("SELECT id FROM resumes WHERE user_id = ?", (target_user_id,))
resume_record = cursor.fetchone()
if not resume_record:
raise HTTPException(status_code=404, detail="Резюме не найдено")
return await get_resume(resume_record["id"])
# ========== ЭНДПОИНТЫ ДЛЯ ОТКЛИКОВ ==========
@app.post("/api/applications")