383 lines
15 KiB
Python

"""
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')
# Ambil data mentah dari Android
foto_input = data.get('foto_base64') or data.get('photo')
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. Insert ke Database
waktu_skrg = datetime.now()
timestamp_str = waktu_skrg.strftime('%Y-%m-%d %H:%M:%S')
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, foto_input, foto_input, status))
# Simpan perubahan & Ambil ID Baru
conn.commit()
new_id = cur.lastrowid
# ==========================================================
# 4. AMBIL ULANG DARI DATABASE (SOLUSI PASTI)
# ==========================================================
# Sesuai permintaan Anda: Kita ambil data yang BARU SAJA masuk
# untuk memastikan variabelnya tidak kosong.
cur.execute("SELECT foto_base64 FROM absensi WHERE id_absensi=%s", (new_id,))
row = cur.fetchone()
# Pastikan kita punya datanya
foto_final = row['foto_base64'] if row else None
cur.close(); conn.close()
# ==========================================================
# 5. KIRIM KE WEBHOOK N8N
# ==========================================================
try:
webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
# Payload dengan Foto ASLI dari Database
webhook_payload = {
"id_absensi": new_id,
"npm": request.user_data['npm'],
"nama": nama_mhs,
"mata_kuliah": nama_matkul,
"latitude": data['latitude'],
"longitude": data['longitude'],
"timestamp": timestamp_str,
"status": status,
"foto_base64": foto_final, # Kirim String Base64 Panjang
}
# Kirim (Timeout agak lama karena Base64 besar)
requests.post(webhook_url, json=webhook_payload, timeout=10)
print(f"✅ Data ID {new_id} terkirim ke N8N (Size foto: {len(str(foto_final))} chars)")
except Exception as e:
print(f"⚠️ Gagal kirim ke N8N: {e}")
# 6. Respon ke 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/<int:id_absensi>', 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)