396 lines
15 KiB
Python
396 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_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()
|
|
|
|
# === FIX ERROR TIMEDELTA DISINI ===
|
|
for j in jadwal:
|
|
# Ubah jam_mulai (timedelta) ke string "HH:MM:SS"
|
|
if isinstance(j.get('jam_mulai'), timedelta):
|
|
j['jam_mulai'] = str(j['jam_mulai'])
|
|
|
|
# Ubah jam_selesai (timedelta) ke string "HH:MM:SS"
|
|
if isinstance(j.get('jam_selesai'), timedelta):
|
|
j['jam_selesai'] = str(j['jam_selesai'])
|
|
|
|
# Cek Status Absensi
|
|
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:
|
|
print(f"Error Jadwal: {e}") # Print error di terminal agar jelas
|
|
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)
|
|
|
|
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()
|
|
|
|
# === FIX ERROR TIMEDELTA DISINI ===
|
|
for item in history:
|
|
if isinstance(item.get('jam_mulai'), timedelta):
|
|
item['jam_mulai'] = str(item['jam_mulai'])
|
|
if isinstance(item.get('jam_selesai'), timedelta):
|
|
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) |