markaz_arshy/app_rev4.py

576 lines
24 KiB
Python
Raw Permalink Normal View History

2025-08-12 14:36:24 +00:00
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)