Update 2025-12-19 19:18:16
This commit is contained in:
Binary file not shown.
Binary file not shown.
32
app.py
32
app.py
@@ -46,6 +46,13 @@ setup_logging(app)
|
|||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# ProxyFix: Nginx/NPM 등 리버스 프록시 뒤에서 실행 시 헤더 신뢰
|
||||||
|
# (HTTPS 인식, 올바른 IP/Scheme 파악으로 CSRF/세션 문제 해결)
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_csrf():
|
def inject_csrf():
|
||||||
@@ -121,31 +128,38 @@ register_routes(app, socketio)
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# 텔레그램 봇 폴링 서비스 (중복 실행 방지 포함)
|
# 텔레그램 봇 폴링 서비스 (중복 실행 방지 포함)
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
_bot_polling_started = False # 전역 플래그로 중복 방지
|
_bot_socket_lock = None
|
||||||
|
|
||||||
|
|
||||||
def start_telegram_bot_polling() -> None:
|
def start_telegram_bot_polling() -> None:
|
||||||
"""텔레그램 봇 폴링을 백그라운드 스레드로 시작 (한 번만 실행)"""
|
"""텔레그램 봇 폴링을 백그라운드 스레드로 시작 (TCP 소켓 락으로 중복 방지)"""
|
||||||
import threading
|
import threading
|
||||||
|
import socket
|
||||||
|
|
||||||
global _bot_polling_started
|
global _bot_socket_lock
|
||||||
|
|
||||||
if _bot_polling_started:
|
if _bot_socket_lock:
|
||||||
app.logger.warning("🤖 텔레그램 봇 폴링은 이미 시작됨 - 중복 요청 무시")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
_bot_polling_started = True
|
app.logger.info("🔒 봇 중복 실행 방지 락(TCP:50000) 획득 시도...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.bind(("127.0.0.1", 50000))
|
||||||
|
s.listen(1)
|
||||||
|
_bot_socket_lock = s
|
||||||
|
app.logger.info("🔒 락 획득 성공! 봇 폴링 스레드를 시작합니다.")
|
||||||
|
except OSError:
|
||||||
|
app.logger.warning("⛔ 락 획득 실패: 이미 다른 프로세스(또는 좀비 프로세스)가 포트 50000을 점유 중입니다. 봇 폴링을 건너뜁니다.")
|
||||||
|
return
|
||||||
|
|
||||||
def _runner():
|
def _runner():
|
||||||
try:
|
try:
|
||||||
# telegram_bot_service.run_polling(app) 호출
|
|
||||||
telegram_run_polling(app)
|
telegram_run_polling(app)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error("텔레그램 봇 폴링 서비스 오류: %s", e)
|
app.logger.error("텔레그램 봇 폴링 서비스 오류: %s", e)
|
||||||
|
|
||||||
polling_thread = threading.Thread(target=_runner, daemon=True)
|
polling_thread = threading.Thread(target=_runner, daemon=True)
|
||||||
polling_thread.start()
|
polling_thread.start()
|
||||||
app.logger.info("🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)")
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Binary file not shown.
@@ -113,14 +113,24 @@ class User(db.Model, UserMixin):
|
|||||||
q = (email or "").strip().lower()
|
q = (email or "").strip().lower()
|
||||||
if not q:
|
if not q:
|
||||||
return None
|
return None
|
||||||
return User.query.filter_by(email=q).first()
|
try:
|
||||||
|
return User.query.filter_by(email=q).first()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"User find_by_email error: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_username(username: Optional[str]) -> Optional["User"]:
|
def find_by_username(username: Optional[str]) -> Optional["User"]:
|
||||||
q = (username or "").strip()
|
q = (username or "").strip()
|
||||||
if not q:
|
if not q:
|
||||||
return None
|
return None
|
||||||
return User.query.filter_by(username=q).first()
|
try:
|
||||||
|
return User.query.filter_by(username=q).first()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"User find_by_username error: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Flask-Login user_loader (SQLAlchemy 2.0 방식)
|
# Flask-Login user_loader (SQLAlchemy 2.0 방식)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from .auth import register_auth_routes
|
|||||||
from .admin import register_admin_routes
|
from .admin import register_admin_routes
|
||||||
from .main import register_main_routes
|
from .main import register_main_routes
|
||||||
from .xml import register_xml_routes
|
from .xml import register_xml_routes
|
||||||
from .utilities import register_util_routes
|
from .utilities import register_util_routes, utils_bp
|
||||||
from .file_view import register_file_view
|
from .file_view import register_file_view
|
||||||
from .jobs import register_jobs_routes
|
from .jobs import register_jobs_routes
|
||||||
from .idrac_routes import register_idrac_routes
|
from .idrac_routes import register_idrac_routes
|
||||||
@@ -21,7 +21,7 @@ def register_routes(app: Flask, socketio=None) -> None:
|
|||||||
register_admin_routes(app)
|
register_admin_routes(app)
|
||||||
register_main_routes(app, socketio)
|
register_main_routes(app, socketio)
|
||||||
register_xml_routes(app)
|
register_xml_routes(app)
|
||||||
register_util_routes(app)
|
app.register_blueprint(utils_bp, url_prefix="/utils")
|
||||||
register_file_view(app)
|
register_file_view(app)
|
||||||
register_jobs_routes(app)
|
register_jobs_routes(app)
|
||||||
register_idrac_routes(app)
|
register_idrac_routes(app)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -270,8 +270,15 @@ def register():
|
|||||||
approval_token=approval_token
|
approval_token=approval_token
|
||||||
)
|
)
|
||||||
user.set_password(form.password.data)
|
user.set_password(form.password.data)
|
||||||
db.session.add(user)
|
|
||||||
db.session.commit()
|
try:
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error("REGISTER: DB commit failed: %s", e)
|
||||||
|
flash("회원가입 처리 중 오류가 발생했습니다. (DB Error)", "danger")
|
||||||
|
return render_template("register.html", form=form)
|
||||||
|
|
||||||
# 텔레그램 알림 (인라인 버튼 포함)
|
# 텔레그램 알림 (인라인 버튼 포함)
|
||||||
message = (
|
message = (
|
||||||
|
|||||||
@@ -207,13 +207,13 @@ def delete_file(filename: str):
|
|||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
try:
|
try:
|
||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
flash(f"{filename} 삭제됨.")
|
flash(f"'{filename}' 파일이 삭제되었습니다.", "success")
|
||||||
logging.info(f"파일 삭제됨: {filename}")
|
logging.info(f"파일 삭제됨: {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"파일 삭제 오류: {e}")
|
logging.error(f"파일 삭제 오류: {e}")
|
||||||
flash("파일 삭제 중 오류가 발생했습니다.", "danger")
|
flash("파일 삭제 중 오류가 발생했습니다.", "danger")
|
||||||
else:
|
else:
|
||||||
flash("파일이 존재하지 않습니다.")
|
flash("파일이 존재하지 않습니다.", "warning")
|
||||||
return redirect(url_for("main.index"))
|
return redirect(url_for("main.index"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -301,68 +301,72 @@ def scan_network():
|
|||||||
지정된 IP 범위(Start ~ End)에 대해 Ping 테스트를 수행하고
|
지정된 IP 범위(Start ~ End)에 대해 Ping 테스트를 수행하고
|
||||||
응답이 있는 IP 목록을 반환합니다.
|
응답이 있는 IP 목록을 반환합니다.
|
||||||
"""
|
"""
|
||||||
import ipaddress
|
|
||||||
import platform
|
|
||||||
import concurrent.futures
|
|
||||||
|
|
||||||
data = request.get_json()
|
|
||||||
start_ip_str = data.get('start_ip')
|
|
||||||
end_ip_str = data.get('end_ip')
|
|
||||||
|
|
||||||
if not start_ip_str or not end_ip_str:
|
|
||||||
return jsonify({"success": False, "error": "시작 IP와 종료 IP를 모두 입력해주세요."}), 400
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_ip = ipaddress.IPv4Address(start_ip_str)
|
import ipaddress
|
||||||
end_ip = ipaddress.IPv4Address(end_ip_str)
|
import platform
|
||||||
|
import concurrent.futures
|
||||||
if start_ip > end_ip:
|
|
||||||
return jsonify({"success": False, "error": "시작 IP가 종료 IP보다 큽니다."}), 400
|
|
||||||
|
|
||||||
# IP 개수 제한 (너무 많은 스캔 방지, 예: C클래스 2개 분량 512개)
|
|
||||||
if int(end_ip) - int(start_ip) > 512:
|
|
||||||
return jsonify({"success": False, "error": "스캔 범위가 너무 넓습니다. (최대 512개)"}), 400
|
|
||||||
|
|
||||||
except ValueError:
|
data = request.get_json(force=True, silent=True) or {}
|
||||||
return jsonify({"success": False, "error": "유효하지 않은 IP 주소 형식입니다."}), 400
|
start_ip_str = data.get('start_ip')
|
||||||
|
end_ip_str = data.get('end_ip')
|
||||||
|
|
||||||
|
if not start_ip_str or not end_ip_str:
|
||||||
|
return jsonify({"success": False, "error": "시작 IP와 종료 IP를 모두 입력해주세요."}), 400
|
||||||
|
|
||||||
# Ping 함수 정의
|
|
||||||
def ping_ip(ip_obj):
|
|
||||||
ip = str(ip_obj)
|
|
||||||
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
|
||||||
timeout_param = '-w' if platform.system().lower() == 'windows' else '-W'
|
|
||||||
# Windows: -w 200 (ms), Linux: -W 1 (s)
|
|
||||||
timeout_val = '200' if platform.system().lower() == 'windows' else '1'
|
|
||||||
|
|
||||||
command = ['ping', param, '1', timeout_param, timeout_val, ip]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# shell=False로 보안 강화, stdout/stderr 무시
|
start_ip = ipaddress.IPv4Address(start_ip_str)
|
||||||
res = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
end_ip = ipaddress.IPv4Address(end_ip_str)
|
||||||
return ip if res.returncode == 0 else None
|
|
||||||
except Exception:
|
if start_ip > end_ip:
|
||||||
return None
|
return jsonify({"success": False, "error": "시작 IP가 종료 IP보다 큽니다."}), 400
|
||||||
|
|
||||||
|
# IP 개수 제한 (너무 많은 스캔 방지, 예: C클래스 2개 분량 512개)
|
||||||
|
if int(end_ip) - int(start_ip) > 512:
|
||||||
|
return jsonify({"success": False, "error": "스캔 범위가 너무 넓습니다. (최대 512개)"}), 400
|
||||||
|
|
||||||
active_ips = []
|
except ValueError:
|
||||||
|
return jsonify({"success": False, "error": "유효하지 않은 IP 주소 형식입니다."}), 400
|
||||||
# IP 리스트 생성
|
|
||||||
# ipaddress 모듈을 사용하여 범위 내 IP 생성
|
|
||||||
target_ips = []
|
|
||||||
temp_ip = start_ip
|
|
||||||
while temp_ip <= end_ip:
|
|
||||||
target_ips.append(temp_ip)
|
|
||||||
temp_ip += 1
|
|
||||||
|
|
||||||
# 병렬 처리 (최대 50 쓰레드)
|
# Ping 함수 정의
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
|
def ping_ip(ip_obj):
|
||||||
results = executor.map(ping_ip, target_ips)
|
ip = str(ip_obj)
|
||||||
|
param = '-n' if platform.system().lower() == 'windows' else '-c'
|
||||||
# 결과 수집 (None 제외)
|
timeout_param = '-w' if platform.system().lower() == 'windows' else '-W'
|
||||||
active_ips = [ip for ip in results if ip is not None]
|
# Windows: -w 200 (ms), Linux: -W 1 (s)
|
||||||
|
timeout_val = '200' if platform.system().lower() == 'windows' else '1'
|
||||||
|
|
||||||
|
command = ['ping', param, '1', timeout_param, timeout_val, ip]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# shell=False로 보안 강화, stdout/stderr 무시
|
||||||
|
res = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
return ip if res.returncode == 0 else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
return jsonify({
|
active_ips = []
|
||||||
"success": True,
|
|
||||||
"active_ips": active_ips,
|
# IP 리스트 생성
|
||||||
"count": len(active_ips),
|
target_ips = []
|
||||||
"message": f"스캔 완료: {len(active_ips)}개의 활성 IP 발견"
|
temp_ip = start_ip
|
||||||
})
|
while temp_ip <= end_ip:
|
||||||
|
target_ips.append(temp_ip)
|
||||||
|
temp_ip += 1
|
||||||
|
|
||||||
|
# 병렬 처리 (최대 50 쓰레드)
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
|
||||||
|
results = executor.map(ping_ip, target_ips)
|
||||||
|
|
||||||
|
# 결과 수집 (None 제외)
|
||||||
|
active_ips = [ip for ip in results if ip is not None]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"active_ips": active_ips,
|
||||||
|
"count": len(active_ips),
|
||||||
|
"message": f"스캔 완료: {len(active_ips)}개의 활성 IP 발견"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Scan network fatal error: {e}")
|
||||||
|
return jsonify({"success": False, "error": f"서버 내부 오류: {str(e)}"}), 500
|
||||||
@@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
await postFormAndHandle(macForm.action);
|
await postFormAndHandle(macForm.action);
|
||||||
location.reload();
|
location.reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('MAC 이동 중 오류: ' + (err?.message || err));
|
alert('MAC 파일 이동 중 오류가 발생했습니다: ' + (err?.message || err));
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = originalHtml;
|
btn.innerHTML = originalHtml;
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
await postFormAndHandle(guidForm.action);
|
await postFormAndHandle(guidForm.action);
|
||||||
location.reload();
|
location.reload();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('GUID 이동 중 오류: ' + (err?.message || err));
|
alert('GUID 파일 이동 중 오류가 발생했습니다: ' + (err?.message || err));
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = originalHtml;
|
btn.innerHTML = originalHtml;
|
||||||
}
|
}
|
||||||
@@ -200,7 +200,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const btnScan = document.getElementById('btnStartScan');
|
const btnScan = document.getElementById('btnStartScan');
|
||||||
if (btnScan) {
|
if (btnScan) {
|
||||||
btnScan.addEventListener('click', async () => {
|
btnScan.addEventListener('click', async () => {
|
||||||
const startIp = '10.10.0.1';
|
const startIp = '10.10.0.2';
|
||||||
const endIp = '10.10.0.255';
|
const endIp = '10.10.0.255';
|
||||||
const ipsTextarea = document.getElementById('ips');
|
const ipsTextarea = document.getElementById('ips');
|
||||||
const progressBar = document.getElementById('progressBar');
|
const progressBar = document.getElementById('progressBar');
|
||||||
@@ -218,7 +218,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
progressBar.textContent = '네트워크 스캔 중... (10.10.0.1 ~ 255)';
|
progressBar.textContent = 'IP 스캔 중...';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -231,6 +231,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
body: JSON.stringify({ start_ip: startIp, end_ip: endIp })
|
body: JSON.stringify({ start_ip: startIp, end_ip: endIp })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 1. 세션 만료로 인한 리다이렉트 감지
|
||||||
|
if (res.redirected) {
|
||||||
|
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. JSON 응답인지 확인
|
||||||
|
const contentType = res.headers.get("content-type");
|
||||||
|
if (!contentType || !contentType.includes("application/json")) {
|
||||||
|
const text = await res.text();
|
||||||
|
if (text.includes("CSRF")) {
|
||||||
|
throw new Error("보안 토큰(CSRF)이 만료되었습니다. 페이지를 새로고침해주세요.");
|
||||||
|
}
|
||||||
|
throw new Error(`서버 응답 오류 (HTTP ${res.status}): ${text.substring(0, 100)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -248,7 +265,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
alert('오류 발생: ' + (err.message || err));
|
alert('오류가 발생했습니다: ' + (err.message || err));
|
||||||
} finally {
|
} finally {
|
||||||
// 상태 복구
|
// 상태 복구
|
||||||
btnScan.disabled = false;
|
btnScan.disabled = false;
|
||||||
@@ -264,4 +281,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// IP 입력 지우기 버튼
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
const btnClear = document.getElementById('btnClearIps');
|
||||||
|
if (btnClear) {
|
||||||
|
btnClear.addEventListener('click', () => {
|
||||||
|
const ipsTextarea = document.getElementById('ips');
|
||||||
|
if (ipsTextarea) {
|
||||||
|
ipsTextarea.value = '';
|
||||||
|
ipsTextarea.dispatchEvent(new Event('input')); // 로컬 스토리지 업데이트 및 카운트 갱신 트리거
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
if (monitoringOn) await fetchJobs(false);
|
if (monitoringOn) await fetchJobs(false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('IP 목록 불러오기 실패: ' + e.message);
|
alert('IP 목록을 불러오는 중 오류가 발생했습니다: ' + e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$btnApply.addEventListener('click', () => {
|
$btnApply.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -70,9 +70,13 @@
|
|||||||
<span class="badge bg-secondary ms-1" id="ipLineCount">0</span>
|
<span class="badge bg-secondary ms-1" id="ipLineCount">0</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex align-items-center gap-1">
|
<div class="d-flex align-items-center gap-1">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="btnStartScan"
|
<button type="button" class="btn btn-sm btn-outline-secondary px-2 py-1" id="btnClearIps"
|
||||||
title="10.10.0.1 ~ 255 자동 스캔">
|
title="입력 내용 지우기" style="font-size: 0.75rem;">
|
||||||
<i class="bi bi-search me-1"></i>IP 자동 스캔 (10.10.0.x)
|
<i class="bi bi-trash me-1"></i>지우기
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary px-2 py-1" id="btnStartScan"
|
||||||
|
title="10.10.0.1 ~ 255 자동 스캔" style="font-size: 0.75rem;">
|
||||||
|
<i class="bi bi-search me-1"></i>IP 스캔
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -220,7 +224,8 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input type="text" class="form-control border-success-subtle form-control-sm"
|
<input type="text" class="form-control border-success-subtle form-control-sm"
|
||||||
name="backup_prefix" placeholder="Prefix" style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
|
name="backup_prefix" placeholder="ex)PO-20251117-0015_20251223_판교_R6615(TY1A)"
|
||||||
|
style="font-size: 0.75rem; padding: 0.2rem 0.5rem;">
|
||||||
<button class="btn btn-success btn-sm px-2" type="submit">
|
<button class="btn btn-success btn-sm px-2" type="submit">
|
||||||
<i class="bi bi-save" style="font-size: 0.75rem;"></i>
|
<i class="bi bi-save" style="font-size: 0.75rem;"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -543,8 +548,8 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/index.js') }}?v={{ range(1, 100000) | random }}"></script>
|
||||||
|
|
||||||
<!-- 외부 script.js 파일 (IP 폼 처리 로직 포함) -->
|
<!-- 외부 script.js 파일 (IP 폼 처리 로직 포함) -->
|
||||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
<script src="{{ url_for('static', filename='script.js') }}?v={{ range(1, 100000) | random }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
10
config.py
10
config.py
@@ -47,6 +47,15 @@ class Config:
|
|||||||
# ── DB (환경변수 DATABASE_URL 있으면 그 값을 우선 사용)
|
# ── DB (환경변수 DATABASE_URL 있으면 그 값을 우선 사용)
|
||||||
sqlite_path = (INSTANCE_DIR / "site.db").as_posix()
|
sqlite_path = (INSTANCE_DIR / "site.db").as_posix()
|
||||||
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", f"sqlite:///{sqlite_path}")
|
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", f"sqlite:///{sqlite_path}")
|
||||||
|
|
||||||
|
# DB 연결 안정성 옵션 (SQLite 락/쓰레드 문제 완화)
|
||||||
|
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||||
|
"pool_pre_ping": True,
|
||||||
|
"pool_recycle": 280,
|
||||||
|
}
|
||||||
|
if SQLALCHEMY_DATABASE_URI.startswith("sqlite"):
|
||||||
|
SQLALCHEMY_ENGINE_OPTIONS["connect_args"] = {"check_same_thread": False}
|
||||||
|
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
|
||||||
# ── Telegram (미설정 시 기능 비활성처럼 동작)
|
# ── Telegram (미설정 시 기능 비활성처럼 동작)
|
||||||
@@ -78,6 +87,7 @@ class Config:
|
|||||||
|
|
||||||
# ── 세션
|
# ── 세션
|
||||||
PERMANENT_SESSION_LIFETIME = timedelta(minutes=int(os.getenv("SESSION_MINUTES", 30)))
|
PERMANENT_SESSION_LIFETIME = timedelta(minutes=int(os.getenv("SESSION_MINUTES", 30)))
|
||||||
|
SESSION_PERMANENT = True # 브라우저 닫아도 세션 유지 (타임아웃까지)
|
||||||
|
|
||||||
# ── SocketIO
|
# ── SocketIO
|
||||||
# threading / eventlet / gevent 중 선택. 기본은 threading (Windows 안정)
|
# threading / eventlet / gevent 중 선택. 기본은 threading (Windows 안정)
|
||||||
|
|||||||
7459
data/logs/app.log
7459
data/logs/app.log
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT
|
|||||||
with flask_app.app_context():
|
with flask_app.app_context():
|
||||||
# 토큰으로 사용자 찾기
|
# 토큰으로 사용자 찾기
|
||||||
user = User.query.filter_by(approval_token=token).first()
|
user = User.query.filter_by(approval_token=token).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
await query.edit_message_text(
|
await query.edit_message_text(
|
||||||
text="❌ 유효하지 않은 승인 요청입니다.\n(이미 처리되었거나 만료된 요청)"
|
text="❌ 유효하지 않은 승인 요청입니다.\n(이미 처리되었거나 만료된 요청)"
|
||||||
@@ -168,6 +168,6 @@ def run_polling(flask_app: Flask) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함
|
# v20 스타일: run_polling 은 동기 함수이고, 내부에서 이벤트 루프를 직접 관리함
|
||||||
application.run_polling(drop_pending_updates=True)
|
application.run_polling(drop_pending_updates=True, stop_signals=[])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error in bot polling: %s", e)
|
logger.exception("Error in bot polling: %s", e)
|
||||||
Reference in New Issue
Block a user