This commit is contained in:
Xander 2025-03-25 21:36:47 +02:00
parent ab4454320a
commit a3723bcdd6
12 changed files with 33673 additions and 2005 deletions

18519
logs/app.log

File diff suppressed because it is too large Load Diff

13
main.py
View File

@ -3,7 +3,8 @@ from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from model.database import init_db from model.database import init_db
from routers import index, logins, users, product, profile, jobs, client from routers import index, logins, users, product, profile, jobs, client, auth1
from fastapi.middleware.cors import CORSMiddleware
import logging import logging
@ -23,6 +24,14 @@ app = FastAPI(title="API для turboapply",
# on_startup=[start_workers] # on_startup=[start_workers]
) )
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Разрешить запросы отовсюду (для тестов)
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Настройка шаблонов # Настройка шаблонов
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@ -35,6 +44,8 @@ app.include_router(logins.router, tags=["login"], include_in_schema=False)
app.include_router(users.router, tags=["users"], include_in_schema=False) app.include_router(users.router, tags=["users"], include_in_schema=False)
app.include_router(product.router, tags=["product"], include_in_schema=False) app.include_router(product.router, tags=["product"], include_in_schema=False)
app.include_router(client.router, tags=["client"]) app.include_router(client.router, tags=["client"])
app.include_router(auth1.router, tags=["auth1"], include_in_schema=False)
# Подключение роутеров # Подключение роутеров

View File

@ -1,7 +1,8 @@
from sqlalchemy import Column, Integer, String, Text, DateTime, Float, Boolean, BigInteger, ForeignKey, DateTime from sqlalchemy import Column, Integer, String, Text, DateTime, Float, Boolean, BigInteger, ForeignKey, DateTime
from sqlalchemy.dialects.mysql import JSON from sqlalchemy.dialects.mysql import JSON
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker, declarative_base
@ -50,9 +51,14 @@ async def init_db():
await session.commit() await session.commit()
# Зависимость FastAPI для работы с БД # Зависимость FastAPI для работы с БД
async def get_async_session() -> AsyncSession: # async def get_async_session() -> AsyncSession:
# async with async_session() as session:
# yield session
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session: async with async_session() as session:
yield session yield session # Сессия остается открытой во время запроса
# Функции для хеширования паролей # Функции для хеширования паролей
def hash_password(password: str) -> str: def hash_password(password: str) -> str:
@ -115,6 +121,7 @@ class Job(Base):
link = Column(String(2083), nullable=True) # URL вакансии link = Column(String(2083), nullable=True) # URL вакансии
link_company = Column(String(2083), nullable=True) # URL компании link_company = Column(String(2083), nullable=True) # URL компании
active = Column(Boolean, default=True) # Вакансия активна? active = Column(Boolean, default=True) # Вакансия активна?
text = Column(Text, nullable=True)
# Связь с AppliedJobs # Связь с AppliedJobs
@ -129,6 +136,9 @@ class Client(Base):
user_login = Column(String(55), unique=True, index=True, nullable=False) # ✅ Правильно user_login = Column(String(55), unique=True, index=True, nullable=False) # ✅ Правильно
user_nicename = Column(String(55), unique=True, index=True, nullable=True) # ✅ Правильно user_nicename = Column(String(55), unique=True, index=True, nullable=True) # ✅ Правильно
user_email = Column(String(55), unique=True, index=True, nullable=True) # ✅ Правильно user_email = Column(String(55), unique=True, index=True, nullable=True) # ✅ Правильно
phone = Column(String(55), unique=True, index=True, nullable=True) # ✅ Правильно
json_data = Column(Text, nullable=True)
# user_registered = Column(String(2083), nullable=True) # URL вакансии # user_registered = Column(String(2083), nullable=True) # URL вакансии
user_status = Column(Boolean, default=True) user_status = Column(Boolean, default=True)

68
routers/auth1.py Normal file
View File

@ -0,0 +1,68 @@
from fastapi import FastAPI, Depends, HTTPException, status, Form, Response, Request, APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from passlib.context import CryptContext
import jwt
from datetime import datetime, timedelta
from model.database import get_async_session, User
SECRET_KEY = "your_secret_key"
REFRESH_SECRET_KEY = "your_refresh_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
async def authenticate_user(db: AsyncSession, username: str, password: str):
result = await db.execute(select(User).where(User.username == username))
user = result.scalars().first()
return user if user and pwd_context.verify(password, user.hashed_password) else None
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/logins")
async def logins(username: str = Form(...), password: str = Form(...), db: AsyncSession = Depends(get_async_session)):
user = await authenticate_user(db, username, password)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Неправильный логин или пароль")
access_token = create_access_token({"sub": user.username, "role": user.role})
refresh_token = create_access_token({"sub": user.username}, expires_delta=timedelta(days=7))
response = Response()
response.set_cookie(key="access_token", value=access_token, httponly=True)
response.set_cookie(key="refresh_token", value=refresh_token, httponly=True)
return {"message": "Успешный вход"}
@router.post("/logout")
async def logout(response: Response):
response.delete_cookie("access_token")
response.delete_cookie("refresh_token")
return {"message": "Вы вышли"}
@router.post("/refresh")
async def refresh_token(request: Request, response: Response):
refresh_token = request.cookies.get("refresh_token")
if not refresh_token:
raise HTTPException(status_code=401, detail="❌ Refresh token отсутствует")
try:
payload = jwt.decode(refresh_token, REFRESH_SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if not username:
raise HTTPException(status_code=401, detail="❌ Ошибка токена")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="❌ Refresh token истёк")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="❌ Ошибка refresh токена")
new_access_token = create_access_token({"sub": username})
response.set_cookie(key="access_token", value=new_access_token, httponly=True)
return {"access_token": new_access_token}

View File

@ -1,9 +1,13 @@
from fastapi import FastAPI, HTTPException, APIRouter, Request, Header from fastapi import FastAPI, HTTPException, APIRouter, Request, Header, Depends
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from typing import Dict from typing import Dict
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import Session
import json import json
from model.database import get_async_session, Client
from utils.clients import upsert_client
from typing import Union
router = APIRouter() router = APIRouter()
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@ -11,7 +15,7 @@ API_KEY = "4545454"
# Пример данных # Пример данных
clients = { clients = {
27: {"username": "John Doe", "email": "john@example.com", "phone": "+123456781"}, 1: {"username": "John Doe", "email": "john@example.com", "phone": "+123456781"},
44: {"username": "Jane Smith", "email": "jane@example.com", "phone": "+987654321"} 44: {"username": "Jane Smith", "email": "jane@example.com", "phone": "+987654321"}
} }
@router.get("/get_client/{client_id}", include_in_schema=False) @router.get("/get_client/{client_id}", include_in_schema=False)
@ -22,17 +26,36 @@ async def get_client_modal(client_id: int):
return JSONResponse(content=client) return JSONResponse(content=client)
class JsonData(BaseModel): class JsonData(BaseModel):
json_data: str json_data: Union[dict, str]
@router.post("/client/") @router.post("/client/")
async def client(data: JsonData, x_api_key: str = Header(None)): async def client(data: JsonData, x_api_key: str = Header(...), db: Session = Depends(get_async_session)):
# Проверяем API-ключ
if x_api_key != "4545454": if x_api_key != "4545454":
raise HTTPException(status_code=403, detail="Invalid API Key") raise HTTPException(status_code=403, detail="Invalid API Key")
# Если json_data строка — декодируем
if isinstance(data.json_data, str):
try:
data.json_data = json.loads(data.json_data)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON format")
print("Полученные данные:", data.json_data)
return {"message": "JSON получен", "data": "ok"}
try:
parsed_data = json.loads(data.json_data) # Декодируем JSON
print("Полученные данные:", parsed_data)
return {"message": "JSON получен", "data": parsed_data} # first_name = parsed_data['first_name']
except json.JSONDecodeError as e: # last_name = parsed_data['last_name']
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}") # email_addr = parsed_data['email_addr']
# user_id = parsed_data['user_id']
# phone_num = parsed_data['phone_num']
# print(first_name)
# # data = await upsert_client(db, first_name, last_name, email_addr, parsed_data)
# client = await upsert_client(db, user_id, first_name, last_name, email_addr, phone_num, str(parsed_data))
# print(f"Received data: data={client}")

View File

@ -52,7 +52,7 @@ async def login(
return Response(content="Неправильный логин или пароль", media_type="text/html") return Response(content="Неправильный логин или пароль", media_type="text/html")
access_token = create_access_token(data={"sub": username, "role": user.role}) access_token = create_access_token(data={"sub": username, "role": user.role})
response = RedirectResponse(url="/", status_code=303) response = RedirectResponse(url="/product", status_code=303)
response.set_cookie(key="access_token", value=access_token, httponly=True) response.set_cookie(key="access_token", value=access_token, httponly=True)
return response return response

File diff suppressed because it is too large Load Diff

12002
search_jobes3.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -28,12 +28,7 @@
</a> </a>
<div class="side-nav__devider my-6"></div> <div class="side-nav__devider my-6"></div>
<ul> <ul>
<li>
<a href="/" class="side-menu {% if current_path == '/' %}side-menu--active{% endif %}">
<div class="side-menu__icon"> <i data-feather="home"></i> </div>
<div class="side-menu__title"> Dashboard </div>
</a>
</li>
<li> <li>
<a href="/product" class="side-menu {% if current_path.startswith('/product') %}side-menu--active{% endif %}"> <a href="/product" class="side-menu {% if current_path.startswith('/product') %}side-menu--active{% endif %}">
<div class="side-menu__icon"> <i data-feather="credit-card"></i> </div> <div class="side-menu__icon"> <i data-feather="credit-card"></i> </div>

View File

@ -31,7 +31,7 @@
<h2 class="text-lg font-medium mr-auto">Add Users</h2> <h2 class="text-lg font-medium mr-auto">Add Users</h2>
<form action="/users/create" method="post"> <form action="/users/create" method="post">
<div class="mt-3"> <label>Email</label> <input type="text" class="input w-full border mt-2" name="username" placeholder="username"> </div> <div class="mt-3"> <label>Username</label> <input type="text" class="input w-full border mt-2" name="username" placeholder="username"> </div>
<div class="mt-3"> <label>Password</label> <input type="password" name="password"class="input w-full border mt-2" placeholder="secret"> </div> <div class="mt-3"> <label>Password</label> <input type="password" name="password"class="input w-full border mt-2" placeholder="secret"> </div>
<label>Role:</label> <label>Role:</label>

View File

@ -47,10 +47,15 @@ def pars_jobs(geo):
} }
for item in search_jobs for item in search_jobs
] ]
# print(search_job)
file_path = "search_jobes2.json"
# file_path = "search_jobes2.json"
# with open(file_path, "w", encoding="utf-8") as json_file:
# json.dump(search_jobes, json_file, indent=4, ensure_ascii=False)
file_path = "search_jobes3.json"
with open(file_path, "w", encoding="utf-8") as json_file: with open(file_path, "w", encoding="utf-8") as json_file:
json.dump(search_jobes, json_file, indent=4, ensure_ascii=False) json.dump(search_jobs, json_file, indent=4, ensure_ascii=False)
print(f"Результаты успешно сохранены в {file_path}") print(f"Результаты успешно сохранены в {file_path}")
@ -59,31 +64,90 @@ def add_to_bd():
#[ ]: Написать функцию записи в БД #[ ]: Написать функцию записи в БД
pass pass
def get_job(job_id): async def get_job(db: AsyncSession, job_id: str):
jobs = api.get_job(job_id) try:
text = jobs['description']['text'] jobs = api.get_job(job_id)
location = jobs['formattedLocation']
title = jobs['title'] # Проверка наличия всех необходимых данных в ответе
listedAt = jobs['listedAt'] required_keys = ["description", "formattedLocation", "title", "listedAt", "companyDetails"]
for key in required_keys:
if key not in jobs:
logging.error(f"❌ Ошибка: Ключ {key} отсутствует в API-ответе")
return None
# Извлечение данных
text = jobs['description']['text']
location = jobs['formattedLocation']
title = jobs['title']
listed_at = jobs['listedAt']
company_info = jobs.get("companyDetails", {}).get("com.linkedin.voyager.deco.jobs.web.shared.WebCompactJobPostingCompany", {}).get("companyResolutionResult", {})
company_name = company_info.get("name", "Unknown")
company_url = company_info.get("url", "")
link = f'https://www.linkedin.com/jobs/view/{job_id}/'
workplace_types = jobs.get("workplaceTypesResolutionResults", {})
# Преобразование из миллисекунд в секунды и конвертация if workplace_types:
future_date = datetime.fromtimestamp(listedAt / 1000) first_key = next(iter(workplace_types)) # Берём первый (и единственный) ключ
# Текущая дата workplace_data = workplace_types[first_key]
current_date = datetime.now()
# Разница в днях localized_name = workplace_data.get("localizedName", "Unknown")
difference = abs((future_date - current_date).days) entity_urn = workplace_data.get("entityUrn", "")
jobPostingId = jobs['jobPostingId']
localizedName = jobs['workplaceTypesResolutionResults']
# Извлекаем все localizedName
localized_names = [value['localizedName'] for value in localizedName.values()]
localized_name = ", ".join(localized_names)
# localized_name = workplaceTypesResolutionResults['urn:li:fs_workplaceType:2']['localizedName']
link = f'https://www.linkedin.com/jobs/view/{job_id}/'
names = jobs['companyDetails']['com.linkedin.voyager.deco.jobs.web.shared.WebCompactJobPostingCompany']['companyResolutionResult']['name']
url = jobs['companyDetails']['com.linkedin.voyager.deco.jobs.web.shared.WebCompactJobPostingCompany']['companyResolutionResult']['url']
logging.info(f"🏢 Тип работы: {localized_name} ({entity_urn})")
else:
localized_name = "Unknown"
entity_urn = ""
# Теперь можно добавить эти значения в БД
# job.location_type = localized_name
# job.entity_urn = entity_urn
# Проверка, есть ли вакансия в базе
query = select(Job).filter(Job.job_id == job_id)
result = await db.execute(query)
job = result.scalars().first()
if job:
logging.info(f"🔄 Обновление вакансии {job_id} в базе...")
job.text = json.dumps(jobs)
job.link = link
job.location = location
job.job_company = company_name
job.link_company = company_url
job.location_type = localized_name
else:
logging.info(f"🆕 Добавление вакансии {job_id} в базу...")
job = Job(
job_id=job_id,
text=json.dumps(jobs),
link=link,
location=location,
job_company=company_name,
link_company=company_url
)
db.add(job)
# Коммит и обновление внутри активной сессии
await db.commit()
await db.refresh(job)
logging.info(f"✅ Вакансия {job_id} успешно сохранена")
return job
except Exception as e:
# await db.rollback() # Откат транзакции при ошибке
logging.error(f"❌ Ошибка при обработке вакансии {job_id}: {e}")
# return None
# except Exception as e:
# await db.rollback()
# logging.error(f"❌ Ошибка при обработке вакансии {job_id}: {e}")
# return None
# [ ]: job_level job_type hourly_rate найти minimum_annual_salary и salary_currency добавил description names Компании url на компанию # [ ]: job_level job_type hourly_rate найти minimum_annual_salary и salary_currency добавил description names Компании url на компанию
logging.info(f"title: {title}, location: {location}, jobPostingId: {jobPostingId}, difference: {difference}, location_type: {localized_name}, link: {link} ===== {names} {url}") #text:{text}, # logging.info(f"title: {title}, location: {location}, jobPostingId: {jobPostingId}, difference: {difference}, location_type: {localized_name}, link: {link} ===== {url}") #text:{text},
@ -132,41 +196,53 @@ async def get_vakansi():
geo = '100025096' # geo = '100025096'
# pars_jobs(geo) # pars_jobs(geo)
# def main(): # async def main():
# get_vakansi() # # get_vakansi()
# logging.info("WORK") # # logging.info("WORK")
# get_job('4130181356') # # jobs =
# # async def main():
# async for db in get_async_session(): # Асинхронный генератор сессий
# query = select(Job).filter(Job.active == 2)
# result = await db.execute(query)
# job = result.scalars().first()
# for j in job:
# ids = j.job_id
# print(ids)
# # await get_job(db, '4192842821')
# # break # Выход после первого использования
# pars_jobs(geo) # # pars_jobs(geo)
# main() # if __name__ == "__main__":
# asyncio.run(main())
from sqlalchemy.orm import sessionmaker, declarative_base # from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine # from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
DATABASE_URL = os.getenv('DATABASE_URL') # DATABASE_URL = os.getenv('DATABASE_URL')
engine = create_async_engine(DATABASE_URL, echo=True) # engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) # async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async_session_maker = sessionmaker( # async_session_maker = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False # engine, class_=AsyncSession, expire_on_commit=False
) # )
async def main(): # async def main():
await get_vakansi() # await get_vakansi()
await engine.dispose() # Корректно закрываем соединение перед завершением # await engine.dispose() # Корректно закрываем соединение перед завершением
if __name__ == "__main__": # if __name__ == "__main__":
asyncio.run(main()) # 🚀 Запускаем программу в единственном event loop # asyncio.run(main()) # 🚀 Запускаем программу в единственном event loop
# [x] Обмен по времени не удалять main() что бы при старте сразу отрабатывала) # [x] Обмен по времени не удалять main() что бы при старте сразу отрабатывала)
# Запуск функции каждые 5 минут # Запуск функции каждые 5 минут
@ -184,3 +260,16 @@ if __name__ == "__main__":
# time.sleep(1) # Пауза между проверками # time.sleep(1) # Пауза между проверками
async def main():
async for db in get_async_session(): # Асинхронный генератор сессий
query = select(Job).filter(Job.active == 2)
result = await db.execute(query)
jobs = result.scalars().all() # Получаем ВСЕ записи в виде списка
for job in jobs:
print(job.job_id)
await get_job(db, job.job_id)
if __name__ == "__main__":
asyncio.run(main())

43
utils/clients.py Normal file
View File

@ -0,0 +1,43 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.exc import IntegrityError
from model.database import Client
async def upsert_client(db: AsyncSession, user_id: str, user_login: str, user_nicename: str, user_email: str, phone: str, json_data: str):
# Проверяем, существует ли клиент с таким логином или email
async with db.begin():
result = await db.execute(
select(Client).filter((Client.id == user_id))# | (Client.user_email == user_email))
)
client = result.scalars().first() # Получаем первый результат или None
if client:
# Если клиент существует, обновляем его данные
client.user_nicename = user_nicename
client.phone = phone
client.json_data = json_data
# Можно добавить другие поля для обновления
try:
await db.commit() # Применяем изменения в базе данных
await db.refresh(client) # Обновляем объект в Python
return client
except IntegrityError:
await db.rollback() # В случае ошибки откатываем изменения
raise
else:
# Если клиента нет, создаем нового
new_client = Client(
user_login=user_login,
user_nicename=user_nicename,
user_email=user_email,
json_data=json_data,
)
db.add(new_client)
try:
await db.commit() # Сохраняем нового клиента в базе данных
await db.refresh(new_client) # Обновляем объект в Python
return new_client
except IntegrityError:
await db.rollback() # В случае ошибки откатываем изменения
raise