v1.3.1
This commit is contained in:
480
server.py
480
server.py
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user