Compare commits

..

10 Commits

Author SHA1 Message Date
e169ad65a0 Изменен способ получения значения capcha_score из переменной окружения. Исправлен порядок сортировки результатов запроса в базе данных на убывание. Исправлены отступы и форматирование кода для соответствия стандартам. Удален ненужный файл правил для Python/Flask. 2025-05-18 19:11:56 +03:00
545b60ff47 Merge branch 'feature/bootstrap5-migration' 2025-05-18 00:52:44 +03:00
27080d10d8 окальные изменения: .gitignore, app.py, requirements.txt — подготовка к слиянию веток 2025-05-18 00:52:13 +03:00
e9949a6ca2 обильная адаптация: скрытие .lead, уменьшение заголовка, закрепление меню, перенос стилей в CSS\n\n- Скрытие текста .lead на мобильных для всех страниц, кроме главной (через .lead-main)\n- меньшение размера .display-4 на мобильных\n- еренос всех мобильных стилей из head.html в static/styles.css\n- акрепление меню .navbar сверху на мобильных, добавлен padding-top у body для корректного отображения контента\n- справлен лишний отступ сверху (теперь используется padding-top у body вместо margin-top у .container-lg)\n- лавная страница: добавлен класс lead-main для сохранения текста на мобильных 2025-05-18 00:48:58 +03:00
Administrator
04ad3163d4 Merge branch 'feature/bootstrap5-migration' into 'main'
feat: обновление API для поиска автомобиля по VIN - изменен маршрут на...

See merge request root/salvagedb!1
2025-05-14 17:29:09 +00:00
5609607c72 Обновлен файл .cursorignore: исключены файлы, не имеющие смысла для контекста проекта (gips.txt, ips.txt, killapp.sh, refresh_ip.sh, sync_cloud_nginx.sh, watchdog.sh) 2025-05-12 17:40:57 +03:00
e52f7ea5d9 Улучшена генерация PDF отчета
- Добавлен OverlayCanvas для отрисовки штампа поверх контента

- Изменено позиционирование штампа

- Добавлены необходимые импорты для работы с Canvas
2025-05-11 02:04:27 +03:00
98b0e7ac26 Добавлена защита от прямого доступа к генерации отчетов
- Добавлена проверка сессии для VIN

- Добавлено ограничение по времени (5 минут)

- Добавлено логирование попыток прямого доступа
2025-05-10 23:57:19 +03:00
2918d9933f ереведены сообщения логирования на английский язык
- аменены русские сообщения на английские в эндпоинте /static/<path:filename>
2025-05-08 11:27:58 +03:00
3e4f75e11d лучшена конфигурация логирования
- обавлена ротация логов по размеру

- обавлены отдельные логгеры для разных компонентов

- лучшен формат сообщений

- аменены все использования app.logger на logger
2025-05-08 11:26:54 +03:00
18 changed files with 626 additions and 131 deletions

View File

@ -1,8 +1,3 @@
---
description:
globs:
alwaysApply: true
---
You are an expert in Python, Flask, and scalable API development.

6
.cursorignore Normal file
View File

@ -0,0 +1,6 @@
gips.txt
ips.txt
killapp.sh
refresh_ip.sh
sync_cloud_nginx.sh
watchdog.sh

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ __pycache__/
*.pyc
logs
node_modules/

274
app.py
View File

@ -20,12 +20,15 @@ from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, mm
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import Frame
import sys
import logging
capcha_score: float = 0.1
capcha_score: float = os.environ.get('CAPCHA_SCORE',0.5)
capcha_site = '6LcJpHMgAAAAAMQLNY_g8J2Kv_qmCGureRN_lbGl'
capcha_site_sec = '6LcJpHMgAAAAAIUf4Jg_7NvawQKZoLoVypDU6-d8'
capcha_site_url='https://www.google.com/recaptcha/api/siteverify'
@ -78,34 +81,104 @@ def save_request(request):
req_data['remote_addr'] = request.remote_addr
return req_data
dictConfig(
{
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s] [%(levelname)s | %(module)s] %(message)s",
"datefmt": " %d/%m/%Y %H:%M:%S"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
},
"file": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": app_path + "/logs/app.log",
"when": "D",
"interval": 10,
"backupCount": 5,
"formatter": "default",
},
},
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
}
)
# Создаем директорию для логов если её нет
log_dir = os.path.join(app_path, 'logs')
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Определяем уровень логирования в зависимости от окружения
log_level = 'DEBUG' if app_debug else 'INFO'
dictConfig({
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
},
"error": {
"format": "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] [%(pathname)s] %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "default",
"filename": os.path.join(log_dir, "app.log"),
"maxBytes": 10485760, # 10MB
"backupCount": 10,
"encoding": "utf8"
},
"error_file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "error",
"filename": os.path.join(log_dir, "error.log"),
"maxBytes": 10485760, # 10MB
"backupCount": 10,
"encoding": "utf8",
"level": "ERROR"
}
},
"loggers": {
"werkzeug": {
"handlers": ["console", "file"],
"level": log_level,
"propagate": False
},
"flask": {
"handlers": ["console", "file"],
"level": log_level,
"propagate": False
},
"app": {
"handlers": ["console", "file", "error_file"],
"level": log_level,
"propagate": False
}
},
"root": {
"level": log_level,
"handlers": ["console", "file", "error_file"]
}
})
# Создаем логгер для приложения
logger = logging.getLogger("app")
class OverlayCanvas(Canvas):
def __init__(self, *args, **kwargs):
Canvas.__init__(self, *args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
def save(self):
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_overlay() # теперь рисуем поверх
Canvas.showPage(self)
Canvas.save(self)
def draw_overlay(self):
stamp_path = os.path.join(app_path, 'static', 'stamp256.png')
if os.path.exists(stamp_path):
self.drawImage(
stamp_path,
x=400, # настроить по ширине
y=400, # настроить по высоте
width=100,
height=100,
mask='auto'
)
@app.after_request
def after_request(response):
@ -161,7 +234,7 @@ def sitemaps_xml(sitemap_id):
sitemap_pages = cur.fetchall()
return render_template('sitemaps.xml', site=site, sitemap_pages=sitemap_pages)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/")
@ -179,7 +252,7 @@ def index_html():
app.cache['maxnum'] = cnt
return render_template('index.html', site=site, cnt=cnt ,capcha_site=capcha_site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/privacy.html")
@ -188,7 +261,7 @@ def privacy_html():
return render_template('privacy.html', site=site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/robots.txt")
@ -196,7 +269,7 @@ def robot_txt():
try:
return render_template('robots.txt', site=site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/decode", methods = ['POST'])
@ -206,12 +279,12 @@ def decode():
g_respone = request.form['g-recaptcha-response']
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
if capcha_check['success'] == False or capcha_check['score'] <capcha_score:
app.logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
app.logger.info(f'Bad google answer: {capcha_check}')
logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
logger.info(f'Bad google answer: {capcha_check}')
abort(401)
return redirect(f'/detail/{vin}.html', 301)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@ -220,7 +293,7 @@ def decodevin_html():
try:
return render_template('decodevin.html', site=site,capcha_site=capcha_site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/database/page<int:page_num>.html")
@ -234,7 +307,7 @@ def database_page_prc(page_num):
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
except:
print(request)
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
cur.execute('select max(pg) from mv_pages')
max_page = int(cur.fetchall()[0][0])
@ -248,7 +321,7 @@ def database_page_prc(page_num):
pgf = cur.fetchall()
return render_template('database.html', site=site, cur_page=pg, max_page=max_page, pg=pgf)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/detail/<vin>.html")
@ -261,7 +334,7 @@ def detail_vin(vin):
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 0, 0, 1])
except:
print(request)
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
@ -274,7 +347,7 @@ def detail_vin(vin):
res = cur.fetchall()
return render_template('details.html', site=site, vin=vin, det=res, capcha_site=capcha_site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@ -284,17 +357,17 @@ def search():
user_ip = get_ip(request) ## определение ip клиента
vin = request.form.get('q')
ua = request.headers.get('User-Agent')
app.logger.info(f'AgeNt: {ua}')
logger.info(f'AgeNt: {ua}')
g_respone = request.form.get('g-recaptcha-response')
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
if capcha_check['success'] == False or capcha_check['score'] <capcha_score:
app.logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
app.logger.info(f'Bad google answer: {capcha_check}')
logger.info(f'Google reuest: {capcha_site_url}?secret={capcha_site_sec}&response={g_respone}')
logger.info(f'Bad google answer: {capcha_check}')
if app_debug==True:
req_data = save_request(request)
app.logger.info(json.dumps(req_data, indent=4, default=str))
logger.info(json.dumps(req_data, indent=4, default=str))
return 'google recaptcha req low score', 401
@ -315,9 +388,14 @@ def search():
res = cur.fetchall()
cur.execute("""select rownum, t.vin, t.title, t.odo, t.odos, t.dem1, t.dem2, t.year||'/'||t.month from salvagedb t where vin = :p1 and svin = substr(:p1,1,10) """, {'p1': vin})
his = cur.fetchall()
# Сохраняем VIN в сессии
session['last_searched_vin'] = vin
session['last_search_time'] = datetime.datetime.now().timestamp()
return render_template('search.html', site=site, vin=vin, det=res, his=his)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
## API
@ -338,7 +416,7 @@ def api_search():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
if len(access_code) > 16 or access_code is None:
ret = {'status': 'incorrect access_code'}
@ -348,7 +426,7 @@ def api_search():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
@ -373,7 +451,7 @@ def api_search():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
## ищем в истории
cur.execute('select t.odo,t.odos,t.title,t.dem1,t.dem2,t.year,t.month from salvagedb t where vin =:p1 and svin = :p2',
@ -404,7 +482,7 @@ def api_search():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
conn.commit()
@ -414,7 +492,7 @@ def api_search():
# nor found
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
response = app.response_class(
response=json.dumps(ret),
status=200,
@ -426,7 +504,7 @@ def api_search():
return response
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@ -441,7 +519,7 @@ def api_restfact():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
@ -460,7 +538,7 @@ def api_restfact():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
else:
ret = {
@ -473,7 +551,7 @@ def api_restfact():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
@app.route("/api/restfact_detail")
@ -487,7 +565,7 @@ def restfact_detail():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
@ -503,9 +581,9 @@ def restfact_detail():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
cur.execute("select vin, ip, status, dt, cost from billing where access_code = :p1 and dt > CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE) - 45 and made = 1 order by id", {'p1': str(access_code)})
cur.execute("select vin, ip, status, dt, cost from billing where access_code = :p1 and dt > CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE) - 45 and made = 1 order by id desc", {'p1': str(access_code)})
res = cur.fetchall()
if len(res) <= 0:
ret = {'status': 'billing not found'}
@ -515,7 +593,7 @@ def restfact_detail():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
else:
dat = []
@ -539,7 +617,7 @@ def restfact_detail():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
## API V2
@ -561,7 +639,7 @@ def api_search_v2():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
if len(access_code) > 16 or access_code is None:
ret = {'status': 'incorrect access_code'}
@ -571,7 +649,7 @@ def api_search_v2():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
@ -596,7 +674,7 @@ def api_search_v2():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
## ищем в истории
cur.execute('''select s.odo,s.odos,s.title,s.dem1,s.dem2,s.year,s.month, json_value(i.jdata, '$.Runs_Drive') RD_Status, json_value(i.jdata, '$.Locate') Sale_Location, json_value(i.jdata, '$.RepCost') as Repear_cost,
@ -626,23 +704,23 @@ def api_search_v2():
'cost': found_price,
'records': dat
}
response = app.response_class(
response = app.response_class(
response=json.dumps(ret),
status=200,
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
if app.debug:
logger.debug(json.dumps(ret))
cur.execute("insert into billing(access_code, vin, ip, status, dt, cost) values (:p1, :p2, :p3, :p4, CAST(SYSTIMESTAMP AT TIME ZONE 'UTC' AS DATE), :p5)",
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
conn.commit()
return response
conn.commit()
return response
else:
# nor found
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
response = app.response_class(
response=json.dumps(ret),
status=200,
@ -654,7 +732,7 @@ def api_search_v2():
return response
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route("/api/v2/reqphoto")
@ -663,7 +741,7 @@ def api_reqimage():
access_code = request.args.get('access_code', None)
vin = request.args.get('vin', None)
user_ip = get_ip(request) ## определение ip клиента
# user_ip = get_ip(request) ## определение ip клиента
# базовые проверки входящих аргументов
if len(vin) != 17 or vin is None:
@ -674,7 +752,7 @@ def api_reqimage():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
if len(access_code) > 16 or access_code is None:
ret = {'status': 'incorrect access_code'}
@ -684,7 +762,7 @@ def api_reqimage():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
conn = pool.acquire()
@ -708,7 +786,7 @@ def api_reqimage():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
cur.execute('select count(*) from salvagedb.salvage_images where vin = :p1 and fn = 1' , {'p1':vin.upper()})
@ -722,7 +800,7 @@ def api_reqimage():
mimetype='application/json'
)
if app.debug:
app.logger.debug(json.dumps(ret))
logger.debug(json.dumps(ret))
return response
else:
cur.execute("""select GenImages('{}','{}') as ui from dual""".format(access_code, vin.upper()))
@ -735,7 +813,7 @@ def api_reqimage():
for it in res:
images.append({
'num':nm,
'url':'https://salimages.salvagedb/{}/{}'.format(req_id, it[0])
'url':'https://salvagedb.com/simages/{}/{}'.format(req_id, it[0])
})
nm = nm + 1
ret = {'status': 'Image_found', 'Images':images}
@ -753,7 +831,7 @@ def api_reqimage():
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@ -762,7 +840,7 @@ def ads_txt():
try:
return render_template('ads.txt')
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route('/favicon.ico')
@ -774,7 +852,7 @@ def rate_limit():
try:
return render_template('rate_limit.html', site=site)
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
return 'bad request!', 500
@app.route('/donate')
@ -809,13 +887,13 @@ def serve_static(filename):
file_ext = os.path.splitext(filename)[1].lower()
if file_ext not in allowed_extensions:
app.logger.warning(f'Попытка доступа к запрещенному типу файла: {filename}')
logger.warning(f'Attempt to access forbidden file type: {filename}')
return 'Access denied', 403
# Проверка пути на directory traversal
safe_path = os.path.normpath(os.path.join('static', filename))
if not safe_path.startswith('static'):
app.logger.warning(f'Попытка доступа к файлу вне директории static: {filename}')
logger.warning(f'Attempt to access file outside static directory: {filename}')
return 'Access denied', 403
# Определение MIME-типа
@ -834,7 +912,7 @@ def serve_static(filename):
mime_type = mime_types.get(file_ext, 'application/octet-stream')
# Логирование доступа
app.logger.info(f'Доступ к статическому файлу: {filename}')
logger.info(f'Access to static file: {filename}')
response = make_response(send_from_directory('static', filename))
response.headers['Content-Type'] = mime_type
@ -848,12 +926,22 @@ def serve_static(filename):
return response
except Exception as e:
app.logger.error(f'Ошибка доступа к файлу {filename}: {str(e)}')
logger.error(f'Error accessing file {filename}: {str(e)}')
return 'File not found', 404
@app.route("/salvagereport/<string:vin>")
def generate_pdf_report(vin):
try:
# Проверяем наличие VIN в сессии и время последнего поиска
if 'last_searched_vin' not in session or session['last_searched_vin'] != vin:
logger.warning(f'Direct access attempt to report generation for VIN: {vin}')
return 'Access denied', 403
# Проверяем время последнего поиска (не более 5 минут)
if datetime.datetime.now().timestamp() - session['last_search_time'] > 300:
logger.warning(f'Report generation attempt expired for VIN: {vin}')
return 'Access denied', 403
conn = pool.acquire()
cur = conn.cursor()
user_ip = get_ip(request)
@ -861,7 +949,7 @@ def generate_pdf_report(vin):
try:
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
except:
app.logger.error(traceback.format_exc())
logger.error(traceback.format_exc())
# Get vehicle details
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
@ -1049,6 +1137,19 @@ def generate_pdf_report(vin):
elements.append(Spacer(1, 15*mm))
# Добавляем штамп поверх таблицы
# stamp_path = os.path.join(app_path, 'static', 'stamp256.png')
# if os.path.exists(stamp_path):
# stamp_img = Image(stamp_path, width=100, height=100)
# stamp_img.hAlign = 'RIGHT'
# stamp_img.vAlign = 'TOP'
# # Размещаем штамп поверх таблицы
# elements.append(stamp_img)
# elements.append(Spacer(1, 15*mm))
# Дисклеймер
disclaimer_text = """
Disclaimer: This report provides information about salvage or junk vehicles; damage from hail, flood or fire;
@ -1062,10 +1163,11 @@ def generate_pdf_report(vin):
elements.append(Spacer(1, 15*mm))
elements.append(Paragraph(f"© {current_year} SALVAGEDB.COM - All Rights Reserved", styles['SalvageDBFooter']))
elements.append(Paragraph("Visit SALVAGEDB.COM for more information about vehicle history", styles['SalvageDBFooter']))
elements.append(Paragraph("This report is provided as is without any guarantees or warranty.", styles['Disclaimer']))
elements.append(Paragraph('"This report is provided as is without any guarantees or warranty."', styles['Disclaimer']))
# Строим PDF документ
doc.build(elements)
# doc.build(elements, onFirstPage=add_stamp_overlay, onLaterPages=add_stamp_overlay)
doc.build(elements, canvasmaker=OverlayCanvas)
# Получаем содержимое буфера
pdf_data = buffer.getvalue()

View File

@ -1,31 +0,0 @@
from PIL import Image
import os
def generate_icons():
# Создаем директорию если её нет
if not os.path.exists('static/icons'):
os.makedirs('static/icons')
# Открываем исходное изображение
source = Image.open('static/Logo2.png')
# Размеры иконок
sizes = [72, 96, 128, 144, 152, 192, 384, 512]
# Генерируем иконки каждого размера
for size in sizes:
# Создаем новое изображение с белым фоном
icon = Image.new('RGBA', (size, size), (255, 255, 255, 0))
# Изменяем размер исходного изображения
resized = source.resize((size, size), Image.Resampling.LANCZOS)
# Вставляем в центр
icon.paste(resized, (0, 0))
# Сохраняем
icon.save(f'static/icons/icon-{size}x{size}.png', 'PNG')
print(f'Создана иконка {size}x{size}')
if __name__ == '__main__':
generate_icons()

View File

@ -1,4 +1,5 @@
flask
oracledb
cacheing
reportlab
reportlab
aiogram

93
rules.md Normal file
View File

@ -0,0 +1,93 @@
You are an expert in Python, Flask, and scalable API development.
Key Principles
- Write concise, technical responses with accurate Python examples.
- Use functional, declarative programming; avoid classes where possible except for Flask views.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., is_active, has_permission).
- Use lowercase with underscores for directories and files (e.g., blueprints/user_routes.py).
- Favor named exports for routes and utility functions.
- Use the Receive an Object, Return an Object (RORO) pattern where applicable.
Python/Flask
- Use def for function definitions.
- Use type hints for all function signatures where possible.
- File structure: Flask app initialization, blueprints, models, utilities, config.
- Avoid unnecessary curly braces in conditional statements.
- For single-line statements in conditionals, omit curly braces.
- Use concise, one-line syntax for simple conditional statements (e.g., if condition: do_something()).
Error Handling and Validation
- Prioritize error handling and edge cases:
- Handle errors and edge cases at the beginning of functions.
- Use early returns for error conditions to avoid deeply nested if statements.
- Place the happy path last in the function for improved readability.
- Avoid unnecessary else statements; use the if-return pattern instead.
- Use guard clauses to handle preconditions and invalid states early.
- Implement proper error logging and user-friendly error messages.
- Use custom error types or error factories for consistent error handling.
Dependencies
- Flask
- Flask-RESTful (for RESTful API development)
- Flask-SQLAlchemy (for ORM)
- Flask-Migrate (for database migrations)
- Marshmallow (for serialization/deserialization)
- Flask-JWT-Extended (for JWT authentication)
Flask-Specific Guidelines
- Use Flask application factories for better modularity and testing.
- Organize routes using Flask Blueprints for better code organization.
- Use Flask-RESTful for building RESTful APIs with class-based views.
- Implement custom error handlers for different types of exceptions.
- Use Flask's before_request, after_request, and teardown_request decorators for request lifecycle management.
- Utilize Flask extensions for common functionalities (e.g., Flask-SQLAlchemy, Flask-Migrate).
- Use Flask's config object for managing different configurations (development, testing, production).
- Implement proper logging using Flask's app.logger.
- Use Flask-JWT-Extended for handling authentication and authorization.
Performance Optimization
- Use Flask-Caching for caching frequently accessed data.
- Implement database query optimization techniques (e.g., eager loading, indexing).
- Use connection pooling for database connections.
- Implement proper database session management.
- Use background tasks for time-consuming operations (e.g., Celery with Flask).
Key Conventions
1. Use Flask's application context and request context appropriately.
2. Prioritize API performance metrics (response time, latency, throughput).
3. Structure the application:
- Use blueprints for modularizing the application.
- Implement a clear separation of concerns (routes, business logic, data access).
- Use environment variables for configuration management.
Database Interaction
- Use Flask-SQLAlchemy for ORM operations.
- Implement database migrations using Flask-Migrate.
- Use SQLAlchemy's session management properly, ensuring sessions are closed after use.
Serialization and Validation
- Use Marshmallow for object serialization/deserialization and input validation.
- Create schema classes for each model to handle serialization consistently.
Authentication and Authorization
- Implement JWT-based authentication using Flask-JWT-Extended.
- Use decorators for protecting routes that require authentication.
Testing
- Write unit tests using pytest.
- Use Flask's test client for integration testing.
- Implement test fixtures for database and application setup.
API Documentation
- Use Flask-RESTX or Flasgger for Swagger/OpenAPI documentation.
- Ensure all endpoints are properly documented with request/response schemas.
Deployment
- Use Gunicorn or uWSGI as WSGI HTTP Server.
- Implement proper logging and monitoring in production.
- Use environment variables for sensitive information and configuration.
Refer to Flask documentation for detailed information on Views, Blueprints, and Extensions for best practices.

BIN
static/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

51
static/manifest.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "Salvagedb",
"short_name": "Salvagedb",
"description": "Check vehicle history and salvage information",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0d6efd",
"icons": [
{
"src": "/static/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/static/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/static/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/static/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/static/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

BIN
static/salvagedblogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
static/stamp256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
static/stamp_big.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1395,4 +1395,24 @@ a.menu:after, .dropdown-toggle:after {
font-size: 16px;
line-height: 1.6;
color: #666;
}
@media (max-width: 576px) {
.lead:not(.lead-main) {
display: none !important;
}
.display-4 {
font-size: 1.6rem;
line-height: 1.2;
}
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1050;
}
body {
padding-top: 56px;
}
}

40
telegram_bot.py Normal file
View File

@ -0,0 +1,40 @@
import os
import requests
from flask import Flask
from telegram import Update, Bot
from telegram.ext import CommandHandler, CallbackContext, ApplicationBuilder
app = Flask(__name__)
TOKEN = 'YOUR_TELEGRAM_BOT_TOKEN' # Замените на ваш токен
async def start(update: Update, context: CallbackContext) -> None:
await update.message.reply_text('Привет! Используйте /decode <vin> для декодирования VIN или /check <vin> для проверки VIN.')
def decode_vin(update: Update, context: CallbackContext) -> None:
vin = context.args[0] if context.args else None
if vin:
response = requests.get(f'http://localhost:5000/search?vin={vin}')
update.message.reply_text(response.text)
else:
update.message.reply_text('Пожалуйста, укажите VIN.')
def check_vin(update: Update, context: CallbackContext) -> None:
vin = context.args[0] if context.args else None
if vin:
response = requests.get(f'http://localhost:5000/detail/{vin}.html')
update.message.reply_text(response.text)
else:
update.message.reply_text('Пожалуйста, укажите VIN.')
def main() -> None:
application = ApplicationBuilder().token(TOKEN).build()
dispatcher = application.dispatcher
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("decode", decode_vin))
dispatcher.add_handler(CommandHandler("check", check_vin))
application.run_polling()
if __name__ == '__main__':
main()

39
templates/base.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ site }}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index_html') }}">{{ site }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index_html') }}">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('decodevin_html') }}">Decode VIN</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('donate') }}">Support Us</a>
</li>
</ul>
</div>
</div>
</nav>
{% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -6,7 +6,7 @@
<div class="container-lg py-4">
<div class="p-5 mb-4 bg-light rounded-3">
<h1 class="display-4">Buying a Used Car? Check it!</h1>
<p class="lead">
<p class="lead lead-main">
<a href="https://www.salvagedb.com/" title="Salvagedb.com" rel="nofollow">Salvagedb.com</a> provides
information about salvage or junk vehicles; damage from hail, flood or fire; mileage discrepancies or
odometer rollback; and gray market vehicles. We do not claim that the car got in our databank has salvage

178
templates/report_pdf.html Normal file
View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vehicle History Report</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #333;
padding-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header p {
font-size: 14px;
color: #666;
}
.report-date {
text-align: right;
font-size: 12px;
margin-bottom: 20px;
}
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 10px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.footer {
margin-top: 50px;
text-align: center;
font-size: 12px;
color: #666;
border-top: 1px solid #ddd;
padding-top: 20px;
}
.disclaimer {
font-size: 10px;
margin-top: 20px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Vehicle History Report</h1>
<p>SALVAGEDB.COM - Comprehensive Vehicle History Check</p>
</div>
<div class="report-date">
<p>Report Date: {{report_date}}</p>
<p>Report ID: {{report_id}}</p>
</div>
<div class="section">
<h2 class="section-title">Vehicle Information</h2>
<table>
<tbody>
<tr>
<td><strong>VIN</strong></td>
<td>{{vin}}</td>
</tr>
<tr>
<td><strong>Make</strong></td>
<td>{{det[0][1]}}</td>
</tr>
<tr>
<td><strong>Model</strong></td>
<td>{{det[0][2]}}</td>
</tr>
<tr>
<td><strong>Year</strong></td>
<td>{{det[0][3]}}</td>
</tr>
<tr>
<td><strong>Body Style</strong></td>
<td>{{det[0][4]}}</td>
</tr>
<tr>
<td><strong>Engine</strong></td>
<td>{{det[0][5]}}</td>
</tr>
<tr>
<td><strong>Cylinders</strong></td>
<td>{{det[0][6]}}</td>
</tr>
<tr>
<td><strong>Drive</strong></td>
<td>{{det[0][7]}}</td>
</tr>
</tbody>
</table>
</div>
{% if his %}
<div class="section">
<h2 class="section-title">Salvage History</h2>
<table>
<thead>
<tr>
<th>VIN</th>
<th>Title</th>
<th>Odometer</th>
<th>Odometer Status</th>
<th>Primary Damage</th>
<th>Secondary Damage</th>
</tr>
</thead>
<tbody>
{% for it in his %}
<tr>
<td>{{it[1]}}</td>
<td>{{it[2]}}</td>
<td>{{it[3]}}</td>
<td>{{it[4]}}</td>
<td>{{it[5]}}</td>
<td>{{it[6]}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="section">
<div class="alert-warning" style="padding: 15px; background-color: #fcf8e3; border: 1px solid #faebcc; color: #8a6d3b;">
<h2 style="font-size: 16px; margin: 0;">Salvage history not found.</h2>
</div>
</div>
{% endif %}
<div class="disclaimer">
<p><strong>Disclaimer:</strong> This report provides information about salvage or junk vehicles; damage from hail, flood or fire; mileage discrepancies or odometer rollback; and gray market vehicles. We do not claim that the car got in our databank has salvage title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle in the past.</p>
</div>
<div class="footer">
<p>&copy; {{current_year}} SalvageDB.com - All Rights Reserved</p>
<p>This report is provided as is without any guarantees or warranty. For additional assistance, please contact us.</p>
</div>
</div>
</body>
</html>

View File

@ -54,13 +54,6 @@
</tr>
</tbody>
</table>
<!-- PDF Report Download Button -->
<div class="mt-3">
<a href="/salvagereport/{{vin}}" class="btn btn-primary w-100">
<i class="fas fa-file-pdf me-2"></i> Download PDF Report
</a>
</div>
</div>
</div>
</div>
@ -98,6 +91,13 @@
</tbody>
</table>
</div>
<!-- PDF Report Download Button -->
<div class="mt-3">
<a href="/salvagereport/{{vin}}" class="btn btn-primary w-100">
<i class="fas fa-file-pdf me-2"></i> Download PDF Report
</a>
</div>
</div>
</div>
{% else %}