Compare commits
10 Commits
c805a22d55
...
e169ad65a0
| Author | SHA1 | Date | |
|---|---|---|---|
| e169ad65a0 | |||
| 545b60ff47 | |||
| 27080d10d8 | |||
| e9949a6ca2 | |||
|
|
04ad3163d4 | ||
| 5609607c72 | |||
| e52f7ea5d9 | |||
| 98b0e7ac26 | |||
| 2918d9933f | |||
| 3e4f75e11d |
@ -1,8 +1,3 @@
|
|||||||
---
|
|
||||||
description:
|
|
||||||
globs:
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
|
|
||||||
You are an expert in Python, Flask, and scalable API development.
|
You are an expert in Python, Flask, and scalable API development.
|
||||||
|
|
||||||
6
.cursorignore
Normal file
6
.cursorignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
gips.txt
|
||||||
|
ips.txt
|
||||||
|
killapp.sh
|
||||||
|
refresh_ip.sh
|
||||||
|
sync_cloud_nginx.sh
|
||||||
|
watchdog.sh
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
logs
|
logs
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|||||||
274
app.py
274
app.py
@ -20,12 +20,15 @@ from reportlab.lib import colors
|
|||||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
|
||||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
from reportlab.lib.units import inch, mm
|
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.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||||
|
from reportlab.platypus import Frame
|
||||||
import sys
|
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 = '6LcJpHMgAAAAAMQLNY_g8J2Kv_qmCGureRN_lbGl'
|
||||||
capcha_site_sec = '6LcJpHMgAAAAAIUf4Jg_7NvawQKZoLoVypDU6-d8'
|
capcha_site_sec = '6LcJpHMgAAAAAIUf4Jg_7NvawQKZoLoVypDU6-d8'
|
||||||
capcha_site_url='https://www.google.com/recaptcha/api/siteverify'
|
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
|
req_data['remote_addr'] = request.remote_addr
|
||||||
return req_data
|
return req_data
|
||||||
|
|
||||||
dictConfig(
|
# Создаем директорию для логов если её нет
|
||||||
{
|
log_dir = os.path.join(app_path, 'logs')
|
||||||
"version": 1,
|
if not os.path.exists(log_dir):
|
||||||
"formatters": {
|
os.makedirs(log_dir)
|
||||||
"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_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
|
@app.after_request
|
||||||
def after_request(response):
|
def after_request(response):
|
||||||
@ -161,7 +234,7 @@ def sitemaps_xml(sitemap_id):
|
|||||||
sitemap_pages = cur.fetchall()
|
sitemap_pages = cur.fetchall()
|
||||||
return render_template('sitemaps.xml', site=site, sitemap_pages=sitemap_pages)
|
return render_template('sitemaps.xml', site=site, sitemap_pages=sitemap_pages)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@ -179,7 +252,7 @@ def index_html():
|
|||||||
app.cache['maxnum'] = cnt
|
app.cache['maxnum'] = cnt
|
||||||
return render_template('index.html', site=site, cnt=cnt ,capcha_site=capcha_site)
|
return render_template('index.html', site=site, cnt=cnt ,capcha_site=capcha_site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/privacy.html")
|
@app.route("/privacy.html")
|
||||||
@ -188,7 +261,7 @@ def privacy_html():
|
|||||||
|
|
||||||
return render_template('privacy.html', site=site)
|
return render_template('privacy.html', site=site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/robots.txt")
|
@app.route("/robots.txt")
|
||||||
@ -196,7 +269,7 @@ def robot_txt():
|
|||||||
try:
|
try:
|
||||||
return render_template('robots.txt', site=site)
|
return render_template('robots.txt', site=site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/decode", methods = ['POST'])
|
@app.route("/decode", methods = ['POST'])
|
||||||
@ -206,12 +279,12 @@ def decode():
|
|||||||
g_respone = request.form['g-recaptcha-response']
|
g_respone = request.form['g-recaptcha-response']
|
||||||
capcha_check = requests.post(url=f'{capcha_site_url}?secret={capcha_site_sec}&response={g_respone}').json()
|
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:
|
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}')
|
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'Bad google answer: {capcha_check}')
|
||||||
abort(401)
|
abort(401)
|
||||||
return redirect(f'/detail/{vin}.html', 301)
|
return redirect(f'/detail/{vin}.html', 301)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
|
|
||||||
@ -220,7 +293,7 @@ def decodevin_html():
|
|||||||
try:
|
try:
|
||||||
return render_template('decodevin.html', site=site,capcha_site=capcha_site)
|
return render_template('decodevin.html', site=site,capcha_site=capcha_site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/database/page<int:page_num>.html")
|
@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])
|
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
|
||||||
except:
|
except:
|
||||||
print(request)
|
print(request)
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
cur.execute('select max(pg) from mv_pages')
|
cur.execute('select max(pg) from mv_pages')
|
||||||
max_page = int(cur.fetchall()[0][0])
|
max_page = int(cur.fetchall()[0][0])
|
||||||
@ -248,7 +321,7 @@ def database_page_prc(page_num):
|
|||||||
pgf = cur.fetchall()
|
pgf = cur.fetchall()
|
||||||
return render_template('database.html', site=site, cur_page=pg, max_page=max_page, pg=pgf)
|
return render_template('database.html', site=site, cur_page=pg, max_page=max_page, pg=pgf)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/detail/<vin>.html")
|
@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])
|
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 0, 0, 1])
|
||||||
except:
|
except:
|
||||||
print(request)
|
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,
|
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,
|
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()
|
res = cur.fetchall()
|
||||||
return render_template('details.html', site=site, vin=vin, det=res, capcha_site=capcha_site)
|
return render_template('details.html', site=site, vin=vin, det=res, capcha_site=capcha_site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
|
|
||||||
@ -284,17 +357,17 @@ def search():
|
|||||||
user_ip = get_ip(request) ## определение ip клиента
|
user_ip = get_ip(request) ## определение ip клиента
|
||||||
vin = request.form.get('q')
|
vin = request.form.get('q')
|
||||||
ua = request.headers.get('User-Agent')
|
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')
|
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()
|
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:
|
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}')
|
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'Bad google answer: {capcha_check}')
|
||||||
if app_debug==True:
|
if app_debug==True:
|
||||||
req_data = save_request(request)
|
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
|
return 'google recaptcha req low score', 401
|
||||||
|
|
||||||
|
|
||||||
@ -315,9 +388,14 @@ def search():
|
|||||||
res = cur.fetchall()
|
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})
|
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()
|
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)
|
return render_template('search.html', site=site, vin=vin, det=res, his=his)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
## API
|
## API
|
||||||
@ -338,7 +416,7 @@ def api_search():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
if len(access_code) > 16 or access_code is None:
|
if len(access_code) > 16 or access_code is None:
|
||||||
ret = {'status': 'incorrect access_code'}
|
ret = {'status': 'incorrect access_code'}
|
||||||
@ -348,7 +426,7 @@ def api_search():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
conn = pool.acquire()
|
conn = pool.acquire()
|
||||||
@ -373,7 +451,7 @@ def api_search():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
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',
|
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'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
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)",
|
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})
|
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@ -414,7 +492,7 @@ def api_search():
|
|||||||
# nor found
|
# nor found
|
||||||
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
response = app.response_class(
|
response = app.response_class(
|
||||||
response=json.dumps(ret),
|
response=json.dumps(ret),
|
||||||
status=200,
|
status=200,
|
||||||
@ -426,7 +504,7 @@ def api_search():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
|
|
||||||
@ -441,7 +519,7 @@ def api_restfact():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
conn = pool.acquire()
|
conn = pool.acquire()
|
||||||
@ -460,7 +538,7 @@ def api_restfact():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
ret = {
|
ret = {
|
||||||
@ -473,7 +551,7 @@ def api_restfact():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@app.route("/api/restfact_detail")
|
@app.route("/api/restfact_detail")
|
||||||
@ -487,7 +565,7 @@ def restfact_detail():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
conn = pool.acquire()
|
conn = pool.acquire()
|
||||||
@ -503,9 +581,9 @@ def restfact_detail():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
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()
|
res = cur.fetchall()
|
||||||
if len(res) <= 0:
|
if len(res) <= 0:
|
||||||
ret = {'status': 'billing not found'}
|
ret = {'status': 'billing not found'}
|
||||||
@ -515,7 +593,7 @@ def restfact_detail():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
dat = []
|
dat = []
|
||||||
@ -539,7 +617,7 @@ def restfact_detail():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
## API V2
|
## API V2
|
||||||
@ -561,7 +639,7 @@ def api_search_v2():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
if len(access_code) > 16 or access_code is None:
|
if len(access_code) > 16 or access_code is None:
|
||||||
ret = {'status': 'incorrect access_code'}
|
ret = {'status': 'incorrect access_code'}
|
||||||
@ -571,7 +649,7 @@ def api_search_v2():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
conn = pool.acquire()
|
conn = pool.acquire()
|
||||||
@ -596,7 +674,7 @@ def api_search_v2():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
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,
|
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,
|
'cost': found_price,
|
||||||
'records': dat
|
'records': dat
|
||||||
}
|
}
|
||||||
response = app.response_class(
|
response = app.response_class(
|
||||||
response=json.dumps(ret),
|
response=json.dumps(ret),
|
||||||
status=200,
|
status=200,
|
||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
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)",
|
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})
|
{'p1': str(access_code), 'p2': str(vin), 'p3': str(user_ip), 'p4': 'FOUND', 'p5': found_price})
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# nor found
|
# nor found
|
||||||
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
ret = {'status': 'NOT FOUND', 'cost': notfound_price}
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
response = app.response_class(
|
response = app.response_class(
|
||||||
response=json.dumps(ret),
|
response=json.dumps(ret),
|
||||||
status=200,
|
status=200,
|
||||||
@ -654,7 +732,7 @@ def api_search_v2():
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route("/api/v2/reqphoto")
|
@app.route("/api/v2/reqphoto")
|
||||||
@ -663,7 +741,7 @@ def api_reqimage():
|
|||||||
access_code = request.args.get('access_code', None)
|
access_code = request.args.get('access_code', None)
|
||||||
vin = request.args.get('vin', 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:
|
if len(vin) != 17 or vin is None:
|
||||||
@ -674,7 +752,7 @@ def api_reqimage():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
if len(access_code) > 16 or access_code is None:
|
if len(access_code) > 16 or access_code is None:
|
||||||
ret = {'status': 'incorrect access_code'}
|
ret = {'status': 'incorrect access_code'}
|
||||||
@ -684,7 +762,7 @@ def api_reqimage():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
conn = pool.acquire()
|
conn = pool.acquire()
|
||||||
@ -708,7 +786,7 @@ def api_reqimage():
|
|||||||
mimetype='application/json'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
cur.execute('select count(*) from salvagedb.salvage_images where vin = :p1 and fn = 1' , {'p1':vin.upper()})
|
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'
|
mimetype='application/json'
|
||||||
)
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.debug(json.dumps(ret))
|
logger.debug(json.dumps(ret))
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
cur.execute("""select GenImages('{}','{}') as ui from dual""".format(access_code, vin.upper()))
|
cur.execute("""select GenImages('{}','{}') as ui from dual""".format(access_code, vin.upper()))
|
||||||
@ -735,7 +813,7 @@ def api_reqimage():
|
|||||||
for it in res:
|
for it in res:
|
||||||
images.append({
|
images.append({
|
||||||
'num':nm,
|
'num':nm,
|
||||||
'url':'https://salimages.salvagedb/{}/{}'.format(req_id, it[0])
|
'url':'https://salvagedb.com/simages/{}/{}'.format(req_id, it[0])
|
||||||
})
|
})
|
||||||
nm = nm + 1
|
nm = nm + 1
|
||||||
ret = {'status': 'Image_found', 'Images':images}
|
ret = {'status': 'Image_found', 'Images':images}
|
||||||
@ -753,7 +831,7 @@ def api_reqimage():
|
|||||||
|
|
||||||
|
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
|
|
||||||
@ -762,7 +840,7 @@ def ads_txt():
|
|||||||
try:
|
try:
|
||||||
return render_template('ads.txt')
|
return render_template('ads.txt')
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
@ -774,7 +852,7 @@ def rate_limit():
|
|||||||
try:
|
try:
|
||||||
return render_template('rate_limit.html', site=site)
|
return render_template('rate_limit.html', site=site)
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return 'bad request!', 500
|
return 'bad request!', 500
|
||||||
|
|
||||||
@app.route('/donate')
|
@app.route('/donate')
|
||||||
@ -809,13 +887,13 @@ def serve_static(filename):
|
|||||||
file_ext = os.path.splitext(filename)[1].lower()
|
file_ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
if file_ext not in allowed_extensions:
|
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
|
return 'Access denied', 403
|
||||||
|
|
||||||
# Проверка пути на directory traversal
|
# Проверка пути на directory traversal
|
||||||
safe_path = os.path.normpath(os.path.join('static', filename))
|
safe_path = os.path.normpath(os.path.join('static', filename))
|
||||||
if not safe_path.startswith('static'):
|
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
|
return 'Access denied', 403
|
||||||
|
|
||||||
# Определение MIME-типа
|
# Определение MIME-типа
|
||||||
@ -834,7 +912,7 @@ def serve_static(filename):
|
|||||||
mime_type = mime_types.get(file_ext, 'application/octet-stream')
|
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 = make_response(send_from_directory('static', filename))
|
||||||
response.headers['Content-Type'] = mime_type
|
response.headers['Content-Type'] = mime_type
|
||||||
@ -848,12 +926,22 @@ def serve_static(filename):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return 'File not found', 404
|
||||||
|
|
||||||
@app.route("/salvagereport/<string:vin>")
|
@app.route("/salvagereport/<string:vin>")
|
||||||
def generate_pdf_report(vin):
|
def generate_pdf_report(vin):
|
||||||
try:
|
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()
|
conn = pool.acquire()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
user_ip = get_ip(request)
|
user_ip = get_ip(request)
|
||||||
@ -861,7 +949,7 @@ def generate_pdf_report(vin):
|
|||||||
try:
|
try:
|
||||||
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
|
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
|
||||||
except:
|
except:
|
||||||
app.logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
# Get vehicle details
|
# 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,
|
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))
|
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_text = """
|
||||||
Disclaimer: This report provides information about salvage or junk vehicles; damage from hail, flood or fire;
|
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(Spacer(1, 15*mm))
|
||||||
elements.append(Paragraph(f"© {current_year} SALVAGEDB.COM - All Rights Reserved", styles['SalvageDBFooter']))
|
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("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 документ
|
# Строим PDF документ
|
||||||
doc.build(elements)
|
# doc.build(elements, onFirstPage=add_stamp_overlay, onLaterPages=add_stamp_overlay)
|
||||||
|
doc.build(elements, canvasmaker=OverlayCanvas)
|
||||||
|
|
||||||
# Получаем содержимое буфера
|
# Получаем содержимое буфера
|
||||||
pdf_data = buffer.getvalue()
|
pdf_data = buffer.getvalue()
|
||||||
|
|||||||
@ -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()
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
flask
|
flask
|
||||||
oracledb
|
oracledb
|
||||||
cacheing
|
cacheing
|
||||||
reportlab
|
reportlab
|
||||||
|
aiogram
|
||||||
93
rules.md
Normal file
93
rules.md
Normal 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
BIN
static/logo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
51
static/manifest.json
Normal file
51
static/manifest.json
Normal 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
BIN
static/salvagedblogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
static/stamp256.png
Normal file
BIN
static/stamp256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
static/stamp_big.png
Normal file
BIN
static/stamp_big.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@ -1395,4 +1395,24 @@ a.menu:after, .dropdown-toggle:after {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #666;
|
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
40
telegram_bot.py
Normal 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
39
templates/base.html
Normal 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>
|
||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="container-lg py-4">
|
<div class="container-lg py-4">
|
||||||
<div class="p-5 mb-4 bg-light rounded-3">
|
<div class="p-5 mb-4 bg-light rounded-3">
|
||||||
<h1 class="display-4">Buying a Used Car? Check it!</h1>
|
<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
|
<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
|
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
|
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
178
templates/report_pdf.html
Normal 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>© {{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>
|
||||||
@ -54,13 +54,6 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -98,6 +91,13 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user