576 lines
No EOL
24 KiB
Python
576 lines
No EOL
24 KiB
Python
import sqlite3
|
|
import os
|
|
import time
|
|
import logging
|
|
import threading
|
|
import json
|
|
import hashlib
|
|
import uuid
|
|
from datetime import datetime, timedelta, date
|
|
from flask import Flask, jsonify, request, render_template, session, redirect, url_for, send_from_directory, flash, g, current_app
|
|
from flask_socketio import SocketIO
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from werkzeug.utils import secure_filename
|
|
import secrets
|
|
from flask_wtf import FlaskForm
|
|
from wtforms import StringField, PasswordField, SubmitField, FileField, HiddenField
|
|
from wtforms.validators import DataRequired, EqualTo, Regexp
|
|
from wtforms import BooleanField
|
|
from dateutil.relativedelta import relativedelta
|
|
from functools import wraps
|
|
|
|
app = Flask(__name__)
|
|
# --- PERUBAHAN: Memuat secret key dari environment variable untuk keamanan ---
|
|
app.secret_key = os.environ.get('FLASK_SECRET_KEY', secrets.token_hex(24))
|
|
app.config['c1b086d4-a681-48df-957f-6fcc35a82f6d'] = app.secret_key
|
|
socketio = SocketIO(app, async_mode='eventlet', cors_allowed_origins="*")
|
|
DATABASE_FILE = 'users.db'
|
|
UPLOAD_FOLDER = 'static/bukti_pembayaran'
|
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
|
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s')
|
|
|
|
# --- Global States ---
|
|
INTERNAL_SECRET_KEY = os.environ.get('INTERNAL_SECRET_KEY')
|
|
if not INTERNAL_SECRET_KEY:
|
|
logging.critical("FATAL: INTERNAL_SECRET_KEY environment variable not set.")
|
|
# Di lingkungan produksi, Anda mungkin ingin keluar dari aplikasi di sini
|
|
# exit(1)
|
|
|
|
last_signal_info = {}
|
|
signal_context_cache = {}
|
|
daily_signal_counts = {} # Key: api_key, Value: {'date': 'YYYY-MM-DD', 'count': X}
|
|
feedback_file_lock = threading.Lock()
|
|
db_write_lock = threading.Lock()
|
|
open_positions_map = {}
|
|
open_positions_lock = threading.Lock()
|
|
SYMBOL_ALIAS_MAP = {
|
|
'XAUUSD': 'XAUUSD', 'XAUUSDc': 'XAUUSD', 'XAUUSDm': 'XAUUSD', 'GOLD': 'XAUUSD',
|
|
'BTCUSD': 'BTCUSD', 'BTCUSDc': 'BTCUSD', 'BTCUSDm': 'BTCUSD',
|
|
}
|
|
|
|
# --- Helper Functions ---
|
|
def get_user_status(api_key: str) -> str:
|
|
"""Mendapatkan status user ('active', 'trial', 'expired', 'invalid') dari API key."""
|
|
if not api_key:
|
|
return 'invalid'
|
|
|
|
db = get_db()
|
|
user = db.execute("SELECT end_date, status FROM users WHERE api_key = ?", (api_key,)).fetchone()
|
|
|
|
if not user:
|
|
return 'invalid'
|
|
|
|
if user['status'] not in ['active', 'trial']:
|
|
return user['status']
|
|
|
|
license_end = datetime.strptime(user['end_date'], '%Y-%m-%d').date()
|
|
if date.today() > license_end:
|
|
return 'expired'
|
|
|
|
return user['status']
|
|
|
|
def get_open_positions(api_key, symbol):
|
|
key = f"{api_key}_{symbol}"
|
|
with open_positions_lock:
|
|
return open_positions_map.get(key, [])
|
|
|
|
def is_too_close_to_open_position(new_entry, open_positions, pip_threshold=100.0, max_age_hours=8):
|
|
now = datetime.now()
|
|
recent_positions = []
|
|
for pos in open_positions:
|
|
try:
|
|
pos_time = datetime.fromisoformat(pos['time'])
|
|
if (now - pos_time) < timedelta(hours=max_age_hours):
|
|
recent_positions.append(pos)
|
|
except (ValueError, KeyError):
|
|
recent_positions.append(pos)
|
|
|
|
for pos in recent_positions:
|
|
try:
|
|
if abs(float(pos['entry']) - float(new_entry)) < pip_threshold:
|
|
return True
|
|
except (ValueError, TypeError):
|
|
continue
|
|
return False
|
|
|
|
def allowed_file(filename):
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
def generate_signal_id(api_key, order_type, timestamp):
|
|
data = f"{api_key}:{order_type}:{timestamp}"
|
|
return hashlib.md5(data.encode()).hexdigest()
|
|
|
|
def get_user_license_details(user_data):
|
|
end_date_obj = datetime.strptime(user_data['end_date'], '%Y-%m-%d').date()
|
|
today = date.today()
|
|
status = user_data['status']
|
|
|
|
if status == 'pending_activation':
|
|
return "Menunggu Aktivasi", "bg-warning"
|
|
elif today <= end_date_obj and status in ['active', 'trial']:
|
|
return "Aktif", "bg-success"
|
|
else:
|
|
return "Kadaluarsa", "bg-danger"
|
|
|
|
def get_db():
|
|
if 'db' not in g:
|
|
g.db = sqlite3.connect(DATABASE_FILE, check_same_thread=False, timeout=30)
|
|
g.db.row_factory = sqlite3.Row
|
|
return g.db
|
|
|
|
@app.teardown_appcontext
|
|
def close_db(e=None):
|
|
db = g.pop('db', None)
|
|
if db is not None:
|
|
db.close()
|
|
|
|
def init_db_data():
|
|
with app.app_context():
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
api_key TEXT UNIQUE NOT NULL,
|
|
start_date TEXT NOT NULL,
|
|
end_date TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'trial',
|
|
proof_filename TEXT DEFAULT NULL,
|
|
duration_pending INTEGER DEFAULT NULL,
|
|
whatsapp_number TEXT UNIQUE
|
|
)
|
|
''')
|
|
try:
|
|
cursor.execute("ALTER TABLE users ADD COLUMN whatsapp_number TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS admins (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL
|
|
)
|
|
''')
|
|
cursor.execute("SELECT COUNT(*) FROM admins WHERE username = 'admin'")
|
|
if cursor.fetchone()[0] == 0:
|
|
default_admin_password = generate_password_hash('admin123')
|
|
cursor.execute("INSERT INTO admins (username, password) VALUES (?, ?)", ('admin', default_admin_password))
|
|
conn.commit()
|
|
|
|
# === FORM CLASSES ===
|
|
class LoginForm(FlaskForm):
|
|
username = StringField('Username', validators=[DataRequired()])
|
|
password = PasswordField('Password', validators=[DataRequired()])
|
|
submit = SubmitField('Login')
|
|
|
|
class RegisterForm(FlaskForm):
|
|
username = StringField('Username', validators=[DataRequired()])
|
|
password = PasswordField('Password', validators=[DataRequired()])
|
|
confirm_password = PasswordField('Ulangi Password', validators=[DataRequired(), EqualTo('password', message='Password dan Ulangi Password harus sama')])
|
|
whatsapp_number = StringField('No. WhatsApp', validators=[DataRequired(), Regexp(r'^\+\d{8,16}$', message='Format Nomor WhatsApp tidak valid (contoh: +6281234567890)')])
|
|
agree_terms = BooleanField('Saya menyetujui Syarat & Ketentuan', validators=[DataRequired(message='Anda harus menyetujui Syarat & Ketentuan')])
|
|
submit = SubmitField('Register')
|
|
|
|
class SubscribeForm(FlaskForm):
|
|
duration = HiddenField('Duration', validators=[DataRequired()])
|
|
proof_file = FileField('Unggah Bukti Pembayaran', validators=[DataRequired()])
|
|
submit = SubmitField('Kirim Bukti Pembayaran')
|
|
|
|
# === ROUTES ===
|
|
@app.before_request
|
|
def require_login():
|
|
# --- PERBAIKAN: Logika otentikasi yang lebih jelas untuk admin dan user ---
|
|
|
|
# 1. Tangani rute admin secara terpisah
|
|
if request.path.startswith('/admin'):
|
|
# Izinkan akses ke halaman login admin itu sendiri
|
|
if request.endpoint == 'admin_login_page':
|
|
return
|
|
# Jika mencoba akses halaman admin lain tanpa sesi, alihkan ke login admin
|
|
if 'admin_id' not in session:
|
|
flash("Anda harus login sebagai admin untuk mengakses halaman ini.", "warning")
|
|
return redirect(url_for('admin_login_page'))
|
|
# Jika sudah login admin, izinkan
|
|
return
|
|
|
|
# 2. Tangani rute pengguna biasa
|
|
public_routes = ['login_page', 'register_page', 'get_signal', 'static', 'status_page', 'receive_signal', 'feedback_trade', 'index', 'home_page', 'panduan_page']
|
|
if request.endpoint in public_routes or (request.endpoint and 'static' in request.endpoint):
|
|
return
|
|
|
|
# 3. Jika rute tidak publik dan user belum login, alihkan ke login user
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('login_page'))
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return redirect(url_for('home_page'))
|
|
|
|
@app.route('/home')
|
|
def home_page():
|
|
if 'user_id' in session:
|
|
return redirect(url_for('dashboard_page'))
|
|
return render_template('home.html', current_year=datetime.now().year)
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
def register_page():
|
|
form = RegisterForm()
|
|
if form.validate_on_submit():
|
|
username = form.username.data
|
|
password = form.password.data
|
|
whatsapp_number = form.whatsapp_number.data
|
|
conn = get_db()
|
|
try:
|
|
api_key = str(uuid.uuid4())
|
|
today = date.today()
|
|
end_date = today + relativedelta(days=7)
|
|
conn.execute('''
|
|
INSERT INTO users (username, password, api_key, start_date, end_date, status, whatsapp_number)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', (username, generate_password_hash(password), api_key, today.isoformat(), end_date.isoformat(), 'trial', whatsapp_number))
|
|
conn.commit()
|
|
flash('Registrasi berhasil! Silakan login.', 'success')
|
|
return redirect(url_for('login_page'))
|
|
except Exception as e:
|
|
flash(f'Gagal registrasi: {e}', 'danger')
|
|
return render_template('register.html', form=form)
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login_page():
|
|
if request.method == 'POST':
|
|
data = request.json
|
|
username = data.get('username')
|
|
password = data.get('password')
|
|
if not username or not password:
|
|
return jsonify({"status": "error", "message": "Username dan password diperlukan"}), 400
|
|
conn = get_db()
|
|
user = conn.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
|
|
if user and check_password_hash(user['password'], password):
|
|
session['user_id'] = user['id']
|
|
session.permanent = True
|
|
return jsonify({"status": "success", "redirect_url": url_for('dashboard_page')})
|
|
return jsonify({"status": "error", "message": "Username atau password salah"}), 401
|
|
form = LoginForm()
|
|
return render_template('login.html', form=form)
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
session.pop('user_id', None)
|
|
flash('Anda telah logout.', 'info')
|
|
return redirect(url_for('login_page'))
|
|
|
|
@app.route('/dashboard')
|
|
def dashboard_page():
|
|
user_id = session.get('user_id')
|
|
if not user_id: return redirect(url_for('login_page'))
|
|
conn = get_db()
|
|
user_data = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
if not user_data: return redirect(url_for('logout'))
|
|
status_text, badge_class = get_user_license_details(user_data)
|
|
|
|
# Find the most recent signal from the available signals
|
|
current_signal = None
|
|
if last_signal_info:
|
|
# Sort signals by timestamp in descending order and pick the first one
|
|
latest_signal_key = sorted(last_signal_info, key=lambda k: last_signal_info[k]['timestamp'], reverse=True)[0]
|
|
current_signal = last_signal_info[latest_signal_key]
|
|
|
|
return render_template('index.html', user=user_data, license_status=status_text, badge_class=badge_class, last_signal=current_signal)
|
|
|
|
@app.route('/lisensi')
|
|
def lisensi_page():
|
|
user_id = session.get('user_id')
|
|
if not user_id: return redirect(url_for('login_page'))
|
|
conn = get_db()
|
|
user_data = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
status_text, badge_class = get_user_license_details(user_data)
|
|
return render_template('lisensi.html', api_key=user_data['api_key'], start_date=user_data['start_date'], end_date=user_data['end_date'], license_status=status_text, badge_class=badge_class)
|
|
|
|
@app.route('/panduan')
|
|
def panduan_page():
|
|
return render_template('panduan.html')
|
|
|
|
@app.route('/download/ea')
|
|
def download_ea():
|
|
if 'user_id' not in session: return redirect(url_for('login_page'))
|
|
try:
|
|
conn = get_db()
|
|
user_info = conn.execute("SELECT status, end_date FROM users WHERE id = ?", (session['user_id'],)).fetchone()
|
|
if not user_info:
|
|
flash("Data pengguna tidak ditemukan.", "danger")
|
|
return redirect(url_for('logout'))
|
|
is_active = datetime.strptime(user_info['end_date'], '%Y-%m-%d').date() >= datetime.now().date() and user_info['status'] in ['trial', 'active']
|
|
if not is_active:
|
|
flash("Lisensi Anda tidak aktif. Silakan perpanjang lisensi untuk men-download.", 'warning')
|
|
return redirect(url_for('dashboard_page'))
|
|
except Exception as e:
|
|
flash(f"Gagal memverifikasi lisensi: {e}", "danger")
|
|
logging.error(f"Error saat verifikasi lisensi download EA untuk user {session.get('user_id')}: {e}", exc_info=True)
|
|
return redirect(url_for('dashboard_page'))
|
|
|
|
ea_directory = os.path.join(app.root_path, 'static')
|
|
ea_filename = 'Esteh AI Update.zip' # Pastikan nama file ini sesuai
|
|
if not os.path.exists(os.path.join(ea_directory, ea_filename)):
|
|
flash("File EA tidak ditemukan di server.", 'danger')
|
|
logging.error(f"File EA '{ea_filename}' tidak ditemukan di direktori: {ea_directory}")
|
|
return redirect(url_for('dashboard_page'))
|
|
|
|
logging.info(f"User {session['user_id']} berhasil men-download EA: {ea_filename}")
|
|
return send_from_directory(directory=ea_directory, path=ea_filename, as_attachment=True)
|
|
@app.route('/subscribe')
|
|
def subscribe_page():
|
|
form = SubscribeForm()
|
|
return render_template('subscribe.html', form=form)
|
|
|
|
@app.route('/upload_proof', methods=['POST'])
|
|
def upload_proof():
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('login_page'))
|
|
form = SubscribeForm()
|
|
if form.validate_on_submit():
|
|
file = form.proof_file.data
|
|
if file and allowed_file(file.filename):
|
|
filename = secure_filename(f"{session['user_id']}_{int(time.time())}_{file.filename}")
|
|
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
|
|
conn = get_db()
|
|
conn.execute("UPDATE users SET proof_filename = ?, duration_pending = ? WHERE id = ?", (filename, form.duration.data, session['user_id']))
|
|
conn.commit()
|
|
flash("Bukti pembayaran berhasil diupload! Silakan tunggu aktivasi.", "success")
|
|
return redirect(url_for('dashboard_page'))
|
|
flash("Format file tidak valid!", "danger")
|
|
return redirect(url_for('subscribe_page'))
|
|
|
|
|
|
@app.route('/status')
|
|
def status_page():
|
|
api_key_to_check = INTERNAL_SECRET_KEY
|
|
if 'user_id' in session:
|
|
conn = get_db()
|
|
user_data = conn.execute("SELECT api_key FROM users WHERE id = ?", (session['user_id'],)).fetchone()
|
|
if user_data:
|
|
api_key_to_check = user_data['api_key']
|
|
current_signal = last_signal_info.get(api_key_to_check, last_signal_info.get(INTERNAL_SECRET_KEY))
|
|
return render_template('status.html', last_signal=current_signal)
|
|
|
|
# === API ENDPOINTS ===
|
|
@app.route('/api/get_signal', methods=['GET'])
|
|
def get_signal():
|
|
api_key = request.args.get('key')
|
|
user_status = get_user_status(api_key)
|
|
|
|
if user_status in ['invalid', 'expired', 'pending_activation']:
|
|
return jsonify({"error": f"Unauthorized. Status: {user_status}."}), 401
|
|
|
|
symbol = request.args.get('symbol', 'XAUUSD').upper()
|
|
mapped_symbol = SYMBOL_ALIAS_MAP.get(symbol, symbol)
|
|
# The key for last_signal_info should be consistent for all users
|
|
signal_data_key = f"INTERNAL_SIGNAL_{mapped_symbol}"
|
|
signal_data = last_signal_info.get(signal_data_key)
|
|
|
|
if not signal_data or (datetime.now() - datetime.strptime(signal_data['timestamp'], '%Y-%m-%d %H:%M:%S')).total_seconds() >= 300:
|
|
return jsonify({"order_type": "WAIT", "reason": "No fresh signal from server."})
|
|
|
|
if user_status == 'trial':
|
|
trial_settings = current_app.config.get('APP_CONFIG', {}).get('trial_user_settings', {})
|
|
signal_id = signal_data.get('signal_id')
|
|
context = signal_context_cache.get(signal_id, {})
|
|
profile_name = context.get('profile_name')
|
|
|
|
allowed_profiles = trial_settings.get('allowed_profiles', [])
|
|
if profile_name and profile_name not in allowed_profiles:
|
|
return jsonify({"order_type": "WAIT", "reason": f"Profile '{profile_name}' requires premium subscription."})
|
|
|
|
today_str = date.today().isoformat()
|
|
user_counts = daily_signal_counts.get(api_key, {'date': today_str, 'count': 0})
|
|
if user_counts['date'] != today_str:
|
|
user_counts = {'date': today_str, 'count': 0}
|
|
|
|
limit = trial_settings.get('daily_signal_limit', 3)
|
|
if user_counts['count'] >= limit:
|
|
return jsonify({"order_type": "WAIT", "reason": f"Daily signal limit ({limit}) reached."})
|
|
|
|
logging.info(f"Trial user {api_key} consuming signal {user_counts.get('count', 0) + 1}/{limit} for today.")
|
|
daily_signal_counts[api_key] = {'date': today_str, 'count': user_counts.get('count', 0) + 1}
|
|
|
|
response_data = signal_data['signal_json'].copy()
|
|
response_data.update({"signal_id": signal_data['signal_id'], "order_type": signal_data['order_type']})
|
|
return jsonify(response_data)
|
|
|
|
@app.route('/api/internal/submit_signal', methods=['POST'])
|
|
def receive_signal():
|
|
global last_signal_info, signal_context_cache
|
|
data = request.json
|
|
if not data: return jsonify({"error": "No data received"}), 400
|
|
|
|
# --- PERUBAHAN: Validasi secret key internal ---
|
|
if data.get('secret_key') != INTERNAL_SECRET_KEY:
|
|
logging.warning("Upaya submit sinyal dengan secret key yang salah ditolak.")
|
|
return jsonify({"error": "Invalid secret key"}), 401
|
|
|
|
api_key = data.get('api_key')
|
|
if not api_key:
|
|
logging.warning("Upaya submit sinyal tanpa API key ditolak.")
|
|
return jsonify({"error": "API key is required"}), 400
|
|
|
|
symbol = data.get('symbol', 'XAUUSD').upper()
|
|
mapped_symbol = SYMBOL_ALIAS_MAP.get(symbol, symbol)
|
|
order_type = data.get('order_type')
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
signal_id = generate_signal_id(api_key, order_type, timestamp)
|
|
|
|
signal_context_cache[signal_id] = {
|
|
"score": data.get('score'), "info": data.get('info'), "timestamp": timestamp,
|
|
"symbol": symbol, "order_type": order_type, "profile_name": data.get('profile_name'),
|
|
"score_components": data.get('score_components')
|
|
}
|
|
|
|
signal_payload = {
|
|
'signal_id': signal_id, 'order_type': order_type,
|
|
'timestamp': timestamp, 'signal_json': data.get('signal_json', {})
|
|
}
|
|
|
|
# The key for last_signal_info should be consistent for all users
|
|
key_to_update = f"INTERNAL_SIGNAL_{mapped_symbol}"
|
|
last_signal_info[key_to_update] = signal_payload
|
|
logging.info(f"📢 Sinyal BARU diterima & disiarkan: Type={order_type}, Simbol={symbol}.")
|
|
|
|
# --- PENGEMBANGAN: Kirim notifikasi real-time ke dashboard ---
|
|
notification_data = {
|
|
"symbol": symbol,
|
|
"order_type": order_type,
|
|
"profile_name": data.get('profile_name'),
|
|
"score": data.get('score'),
|
|
"entry_price": data.get('signal_json', {}).get(f"{order_type.capitalize()}Entry", "N/A")
|
|
}
|
|
socketio.emit('new_signal_notification', notification_data)
|
|
|
|
return jsonify({"message": "Signal received"}), 200
|
|
|
|
@app.route("/api/feedback_trade", methods=["POST"])
|
|
def feedback_trade():
|
|
data = request.json
|
|
if not data: return jsonify({"status": "error", "message": "No data received"}), 400
|
|
|
|
signal_id = data.get('signal_id')
|
|
context = signal_context_cache.pop(signal_id, {})
|
|
full_feedback = {**context, **data}
|
|
|
|
feedback_path = "trade_feedback.json"
|
|
with feedback_file_lock:
|
|
try:
|
|
with open(feedback_path, "r+") as f:
|
|
current_data = json.load(f)
|
|
current_data.append(full_feedback)
|
|
f.seek(0)
|
|
json.dump(current_data, f, indent=2)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
with open(feedback_path, "w") as f:
|
|
json.dump([full_feedback], f, indent=2)
|
|
return jsonify({"status": "success"}), 200
|
|
|
|
# === ADMIN PANEL ROUTES ===
|
|
def admin_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'admin_id' not in session:
|
|
flash("Anda harus login sebagai admin untuk mengakses halaman ini.", "warning")
|
|
return redirect(url_for('admin_login_page'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
@app.route('/admin/login', methods=['GET', 'POST'])
|
|
def admin_login_page():
|
|
form = LoginForm()
|
|
if form.validate_on_submit():
|
|
username = form.username.data
|
|
password = form.password.data
|
|
conn = get_db()
|
|
admin = conn.execute("SELECT * FROM admins WHERE username = ?", (username,)).fetchone()
|
|
if admin and check_password_hash(admin['password'], password):
|
|
session['admin_id'] = admin['id']
|
|
flash('Login admin berhasil!', 'success')
|
|
return redirect(url_for('admin_dashboard_page'))
|
|
else:
|
|
flash('Username atau password admin salah.', 'danger')
|
|
return render_template('admin_login.html', form=form)
|
|
|
|
@app.route('/admin/logout')
|
|
@admin_required
|
|
def admin_logout():
|
|
session.pop('admin_id', None)
|
|
flash('Anda telah logout dari panel admin.', 'info')
|
|
return redirect(url_for('admin_login_page'))
|
|
|
|
@app.route('/admin/dashboard')
|
|
@admin_required
|
|
def admin_dashboard_page():
|
|
conn = get_db()
|
|
users = conn.execute("SELECT id, username, start_date, end_date, status, proof_filename FROM users ORDER BY id").fetchall()
|
|
return render_template('admin_dashboard.html', users=users)
|
|
|
|
@app.route('/admin/extend_license', methods=['POST'])
|
|
@admin_required
|
|
def extend_license():
|
|
user_id = request.form.get('user_id')
|
|
days_to_add = int(request.form.get('days', 30))
|
|
|
|
conn = get_db()
|
|
user = conn.execute("SELECT end_date, status FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
|
|
if user:
|
|
current_end_date = datetime.strptime(user['end_date'], '%Y-%m-%d').date()
|
|
# Jika lisensi sudah kadaluarsa, perpanjang dari hari ini
|
|
if current_end_date < date.today():
|
|
new_end_date = date.today() + timedelta(days=days_to_add)
|
|
else:
|
|
new_end_date = current_end_date + timedelta(days=days_to_add)
|
|
|
|
# Jika statusnya pending, ubah jadi active
|
|
new_status = 'active'
|
|
|
|
conn.execute(
|
|
"UPDATE users SET end_date = ?, status = ?, duration_pending = NULL, proof_filename = NULL WHERE id = ?",
|
|
(new_end_date.isoformat(), new_status, user_id)
|
|
)
|
|
conn.commit()
|
|
flash(f"Lisensi untuk user ID {user_id} berhasil diperpanjang selama {days_to_add} hari.", "success")
|
|
else:
|
|
flash(f"User ID {user_id} tidak ditemukan.", "danger")
|
|
|
|
return redirect(url_for('admin_dashboard_page'))
|
|
|
|
|
|
# === SOCKET.IO EVENTS ===
|
|
@socketio.on('connect')
|
|
def handle_connect():
|
|
logging.info(f"Browser client terhubung: {request.sid}")
|
|
|
|
@socketio.on('disconnect')
|
|
def handle_disconnect():
|
|
logging.info(f"Browser client terputus: {request.sid}")
|
|
|
|
@socketio.on('submit_log')
|
|
def handle_submit_log(data):
|
|
socketio.emit('new_log', {'message': data.get('message', '')})
|
|
|
|
# ========== INIT & RUN ==========
|
|
def load_app_config():
|
|
try:
|
|
with open('config.json', 'r') as f:
|
|
app.config['APP_CONFIG'] = json.load(f)
|
|
logging.info("Konfigurasi aplikasi dimuat dari config.json")
|
|
except Exception as e:
|
|
logging.error(f"FATAL: Tidak dapat memuat config.json: {e}")
|
|
app.config['APP_CONFIG'] = {}
|
|
|
|
if __name__ == '__main__':
|
|
os.makedirs(os.path.join(app.root_path, 'static'), exist_ok=True)
|
|
os.makedirs(os.path.join(app.root_path, app.config['UPLOAD_FOLDER']), exist_ok=True)
|
|
with app.app_context():
|
|
init_db_data()
|
|
load_app_config()
|
|
socketio.run(app, host='0.0.0.0', port=5000, debug=False) |