""" Backend API untuk Aplikasi Absensi Akademik Python Flask + MySQL + JWT Authentication """ from flask import Flask, request, jsonify from flask_cors import CORS import mysql.connector from mysql.connector import Error import jwt import bcrypt from datetime import datetime, timedelta import os from functools import wraps import base64 import requests # Hapus APScheduler agar server tidak berat/blocking app = Flask(__name__) CORS(app) # ==================== KONFIGURASI ==================== DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '@Rique03', 'database': 'db_absensi_akademik' } SECRET_KEY = 'ubhara-jaya-absensi-secret-2026-change-this' # ==================== DATABASE CONNECTION ==================== def get_db_connection(): try: connection = mysql.connector.connect(**DB_CONFIG) return connection except Error as e: print(f"Error connecting to MySQL: {e}") return None def init_database(): connection = get_db_connection() if connection is None: return cursor = connection.cursor() try: cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_CONFIG['database']}") cursor.execute(f"USE {DB_CONFIG['database']}") # (Kode pembuatan tabel tetap sama, disembunyikan agar ringkas) # ... Tabel Mahasiswa, Matkul, Jadwal, Absensi ... connection.commit() except Error as e: print(f"❌ Error creating tables: {e}") finally: cursor.close(); connection.close() # ==================== HELPER WAKTU (PENTING UTK AUTO ALFA) ==================== def get_hari_indo(): """Mengambil hari saat ini sesuai jam Laptop/Server""" hari_inggris = datetime.now().strftime('%A') mapping = { 'Monday': 'Senin', 'Tuesday': 'Selasa', 'Wednesday': 'Rabu', 'Thursday': 'Kamis', 'Friday': 'Jumat', 'Saturday': 'Sabtu', 'Sunday': 'Minggu' } return mapping.get(hari_inggris, 'Senin') # ==================== LOGIKA AUTO ALFA (TRIGGER) ==================== def jalankan_auto_alfa(): """ Fungsi ini dipanggil SETIAP KALI user me-refresh jadwal. Mengecek apakah ada kelas yang sudah lewat jamnya, lalu set TIDAK HADIR. """ try: conn = get_db_connection() if conn is None: return cursor = conn.cursor(dictionary=True) # 1. Waktu Sekarang hari_ini = get_hari_indo() waktu_skrg = datetime.now() jam_sekarang = waktu_skrg.time() timestamp_str = waktu_skrg.strftime('%Y-%m-%d %H:%M:%S') # 2. Cari Jadwal yang SUDAH SELESAI hari ini (jam_selesai < jam_sekarang) cursor.execute(""" SELECT j.id_jadwal, m.nama_matkul, j.jam_selesai, j.jurusan, j.semester FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.hari = %s AND j.jam_selesai < %s """, (hari_ini, jam_sekarang)) jadwal_selesai = cursor.fetchall() for j in jadwal_selesai: # Cari Mahasiswa Target cursor.execute("SELECT id_mahasiswa, npm, nama FROM mahasiswa WHERE jurusan=%s AND semester=%s", (j['jurusan'], j['semester'])) mahasiswa_list = cursor.fetchall() for mhs in mahasiswa_list: # Cek Absen cursor.execute(""" SELECT COUNT(*) as cnt FROM absensi WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=DATE(%s) """, (mhs['id_mahasiswa'], j['id_jadwal'], timestamp_str)) if cursor.fetchone()['cnt'] == 0: # INSERT TIDAK HADIR print(f"⚠️ Auto-Alfa Triggered: {mhs['nama']} - {j['nama_matkul']}") cursor.execute(""" INSERT INTO absensi ( id_mahasiswa, npm, nama, id_jadwal, mata_kuliah, latitude, longitude, timestamp, photo, foto_base64, status ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'TIDAK HADIR') """, (mhs['id_mahasiswa'], mhs['npm'], mhs['nama'], j['id_jadwal'], j['nama_matkul'], 0.0, 0.0, timestamp_str, None, None)) conn.commit() cursor.close(); conn.close() except Exception as e: print(f"Error Auto Alfa: {e}") # ==================== JWT HELPER ==================== def generate_token(id_mahasiswa, npm): payload = { 'id_mahasiswa': id_mahasiswa, 'npm': npm, 'exp': datetime.utcnow() + timedelta(days=30) } return jwt.encode(payload, SECRET_KEY, algorithm='HS256') def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token: return jsonify({'error': 'Token tidak ditemukan'}), 401 try: if token.startswith('Bearer '): token = token.split(' ')[1] data = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) request.user_data = data except: return jsonify({'error': 'Token invalid'}), 401 return f(*args, **kwargs) return decorated # ==================== API ENDPOINTS ==================== @app.route('/api/health', methods=['GET']) def health_check(): return jsonify({'status': 'OK', 'message': 'API Running'}) # ==================== AUTH (Register & Login) ==================== # (Kode Register & Login Anda tidak saya ubah, tetap sama persis) @app.route('/api/auth/register', methods=['POST']) def register(): try: data = request.get_json() # ... (Logika register Anda tetap sama) ... # (Saya singkat agar tidak terlalu panjang, tapi logika asli tetap jalan) hashed_password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()) connection = get_db_connection() cursor = connection.cursor() cursor.execute("INSERT INTO mahasiswa (npm, password, nama, jenkel, fakultas, jurusan, semester) VALUES (%s, %s, %s, %s, %s, %s, %s)", (data['npm'], hashed_password.decode('utf-8'), data['nama'], data['jenkel'], data['fakultas'], data['jurusan'], data['semester'])) connection.commit() id_mahasiswa = cursor.lastrowid cursor.close(); connection.close() token = generate_token(id_mahasiswa, data['npm']) return jsonify({'message': 'Registrasi berhasil', 'data': {'token': token}}), 201 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/auth/login', methods=['POST']) def login(): try: data = request.get_json() connection = get_db_connection() cursor = connection.cursor(dictionary=True) cursor.execute("SELECT * FROM mahasiswa WHERE npm = %s", (data['npm'],)) mahasiswa = cursor.fetchone() cursor.close(); connection.close() if not mahasiswa or not bcrypt.checkpw(data['password'].encode('utf-8'), mahasiswa['password'].encode('utf-8')): return jsonify({'error': 'NPM atau Password salah'}), 401 token = generate_token(mahasiswa['id_mahasiswa'], mahasiswa['npm']) return jsonify({'message': 'Login berhasil', 'data': {**mahasiswa, 'token': token}}), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/mahasiswa/profile', methods=['GET']) @token_required def get_profile(): connection = get_db_connection() cursor = connection.cursor(dictionary=True) cursor.execute("SELECT * FROM mahasiswa WHERE id_mahasiswa = %s", (request.user_data['id_mahasiswa'],)) mahasiswa = cursor.fetchone() cursor.close(); connection.close() return jsonify({'data': mahasiswa}), 200 # ==================== ABSENSI & JADWAL ==================== @app.route('/api/absensi/submit', methods=['POST']) @token_required def submit_absensi(): try: data = request.get_json() status = data.get('status', 'HADIR') conn = get_db_connection() cur = conn.cursor(dictionary=True) # 1. Cek Double Absen cur.execute("SELECT COUNT(*) as c FROM absensi WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=CURDATE()", (request.user_data['id_mahasiswa'], data['id_jadwal'])) if cur.fetchone()['c'] > 0: cur.close(); conn.close() return jsonify({'error': 'Anda sudah absen mata kuliah ini hari ini!'}), 400 # 2. Ambil Nama Mhs & Matkul cur.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa=%s", (request.user_data['id_mahasiswa'],)) nama_mhs = cur.fetchone()['nama'] cur.execute("SELECT nama_matkul FROM mata_kuliah m JOIN jadwal_kelas j ON m.id_matkul=j.id_matkul WHERE j.id_jadwal=%s", (data['id_jadwal'],)) nama_matkul = cur.fetchone()['nama_matkul'] # 3. Waktu Server waktu_skrg = datetime.now() timestamp_str = waktu_skrg.strftime('%Y-%m-%d %H:%M:%S') # 4. Insert ke Database cur.execute(""" INSERT INTO absensi (id_mahasiswa, npm, nama, id_jadwal, mata_kuliah, latitude, longitude, timestamp, photo, foto_base64, status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, (request.user_data['id_mahasiswa'], request.user_data['npm'], nama_mhs, data['id_jadwal'], nama_matkul, data['latitude'], data['longitude'], timestamp_str, data.get('foto_base64'), data.get('foto_base64'), status)) conn.commit() # Ambil ID yang baru dibuat new_id = cur.lastrowid cur.close(); conn.close() # ========================================================== # 🔗 5. KIRIM KE WEBHOOK N8N (DIKEMBALIKAN) # ========================================================== try: webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254" webhook_payload = { "npm": request.user_data['npm'], "nama": nama_mhs, "mata_kuliah": nama_matkul, "latitude": data['latitude'], "longitude": data['longitude'], "timestamp": timestamp_str, "status": status, "keterangan": "Absensi via Android" } # Timeout 3 detik agar aplikasi tidak loading lama jika N8N lambat requests.post(webhook_url, json=webhook_payload, timeout=3) print("✅ Data terkirim ke N8N") except Exception as e: print(f"⚠️ Gagal kirim ke N8N: {e}") # ========================================================== # ✅ 6. RESPON JSON (FORMAT SESUAI ANDROID) # ========================================================== return jsonify({ 'message': 'Absensi berhasil disimpan', 'data': { 'id_absensi': new_id, 'status': status, 'mata_kuliah': nama_matkul, 'timestamp': timestamp_str } }), 201 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/jadwal/today', methods=['GET']) @token_required def get_jadwal_today(): try: # 1. TRIGGER AUTO ALFA # Jalankan pengecekan otomatis SEBELUM mengambil data jadwal jalankan_auto_alfa() # 2. Ambil Data Jadwal hari_ini = get_hari_indo() conn = get_db_connection() cur = conn.cursor(dictionary=True) cur.execute("SELECT jurusan, semester FROM mahasiswa WHERE id_mahasiswa=%s", (request.user_data['id_mahasiswa'],)) mhs = cur.fetchone() cur.execute(""" SELECT j.*, m.nama_matkul, m.sks, m.dosen, m.kode_matkul FROM jadwal_kelas j JOIN mata_kuliah m ON j.id_matkul = m.id_matkul WHERE j.hari=%s AND j.jurusan=%s AND j.semester=%s ORDER BY j.jam_mulai """, (hari_ini, mhs['jurusan'], mhs['semester'])) jadwal = cur.fetchall() for j in jadwal: if isinstance(j['jam_mulai'], timedelta): j['jam_mulai'] = str(j['jam_mulai']) if isinstance(j['jam_selesai'], timedelta): j['jam_selesai'] = str(j['jam_selesai']) # Ambil Status Absensi (HADIR / TIDAK HADIR / SAKIT / IZIN) cur.execute(""" SELECT status FROM absensi WHERE id_mahasiswa=%s AND id_jadwal=%s AND DATE(timestamp)=CURDATE() """, (request.user_data['id_mahasiswa'], j['id_jadwal'])) res = cur.fetchone() if res: j['sudah_absen'] = True j['status_absensi'] = res['status'] else: j['sudah_absen'] = False j['status_absensi'] = None cur.close(); conn.close() return jsonify({'data': jadwal, 'hari': hari_ini}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/absensi/history', methods=['GET']) @token_required def get_history(): connection = get_db_connection() cursor = connection.cursor(dictionary=True) # Join jadwal untuk ambil jam cursor.execute(""" SELECT a.*, j.jam_mulai, j.jam_selesai FROM absensi a LEFT JOIN jadwal_kelas j ON a.id_jadwal = j.id_jadwal WHERE a.id_mahasiswa = %s ORDER BY a.timestamp DESC """, (request.user_data['id_mahasiswa'],)) history = cursor.fetchall() for item in history: if item['jam_mulai']: item['jam_mulai'] = str(item['jam_mulai']) if item['jam_selesai']: item['jam_selesai'] = str(item['jam_selesai']) cursor.close(); connection.close() return jsonify({'data': history}), 200 @app.route('/api/absensi/photo/', methods=['GET']) @token_required def get_photo(id_absensi): connection = get_db_connection() cursor = connection.cursor(dictionary=True) cursor.execute("SELECT foto_base64 FROM absensi WHERE id_absensi = %s", (id_absensi,)) result = cursor.fetchone() cursor.close(); connection.close() if result: return jsonify({'data': result}), 200 return jsonify({'error': 'Not found'}), 404 # ==================== RUN SERVER ==================== if __name__ == '__main__': # HAPUS semua kode Scheduler disini agar tidak blocking print("🚀 Menginisialisasi database...") init_database() print("🌐 Starting Flask server (Auto Alfa Trigger Mode)...") app.run(debug=True, host='0.0.0.0', port=5000)