Compare commits

...

2 Commits

3 changed files with 101 additions and 62 deletions

View File

@ -1874,26 +1874,71 @@ fun AbsensiScreenWithJadwal(
})
}
// ... (Kode LocationPermissionLauncher & CameraLauncher SAMA SEPERTI SEBELUMNYA, SALIN DI SINI) ...
// ... Agar kode tidak kepanjangan, saya asumsikan Anda menyalin launcher location/camera dari kode lama ...
// ... PASTIKAN variable 'locationPermissionLauncher' dan 'cameraLauncher' ada di sini ...
val locationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
if (location != null) {
latitude = location.latitude
longitude = location.longitude
val jarak = hitungJarak(location.latitude, location.longitude, AppConstants.KAMPUS_LATITUDE, AppConstants.KAMPUS_LONGITUDE)
jarakKeKampus = jarak
val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) "✅ DI DALAM AREA" else "❌ DI LUAR AREA"
lokasi = "📍 Lat: ${String.format("%.6f", location.latitude)}\n📍 Lon: ${String.format("%.6f", location.longitude)}\n📏 Jarak: ${String.format("%.0f", jarak)} m\n$statusLokasi"
} else { lokasi = "❌ Lokasi tidak tersedia" }
}
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
// ========================================================
// 🛡️ SECURITY FIX: DETEKSI FAKE GPS
// ========================================================
val isFakeGps = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
location.isMock // Untuk Android 12 ke atas
} else {
location.isFromMockProvider // Untuk Android lama
}
if (isFakeGps) {
// JIKA TERDETEKSI PALSU
latitude = null // Null-kan agar tombol kirim mati
longitude = null
jarakKeKampus = null
lokasi = "⛔ FAKE GPS TERDETEKSI!\nSistem menolak lokasi palsu.\nMatikan aplikasi Fake GPS Anda."
// Tampilkan dialog error
errorMessage = "⚠️ Keamanan: Terdeteksi menggunakan Fake GPS/Lokasi Palsu. Mohon matikan aplikasi tersebut dan coba lagi."
} else {
// JIKA LOKASI ASLI (Logika Normal)
latitude = location.latitude
longitude = location.longitude
val jarak = hitungJarak(
location.latitude,
location.longitude,
AppConstants.KAMPUS_LATITUDE,
AppConstants.KAMPUS_LONGITUDE
)
jarakKeKampus = jarak
val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) {
"✅ DI DALAM AREA"
} else {
"❌ DI LUAR AREA"
}
lokasi = "📍 Lat: ${String.format("%.6f", location.latitude)}\n" +
"📍 Lon: ${String.format("%.6f", location.longitude)}\n" +
"📏 Jarak: ${String.format("%.0f", jarak)} m\n" +
"$statusLokasi"
}
} else {
lokasi = "❌ Lokasi tidak tersedia (Aktifkan GPS)"
}
}
.addOnFailureListener {
lokasi = "❌ Gagal mengambil lokasi"
errorMessage = "Gagal mengambil lokasi: ${it.message}"
}
}
} else {
errorMessage = "⚠️ Izin lokasi ditolak. Aplikasi tidak dapat digunakan."
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View File

@ -424,25 +424,16 @@ def get_profile():
@token_required
def submit_absensi():
"""
Endpoint untuk submit absensi (UPDATE: dengan validasi jadwal)
Request Body:
{
"id_jadwal": 1,
"latitude": -6.223276,
"longitude": 107.009273,
"timestamp": "2026-01-13 14:30:00",
"foto_base64": "base64_string",
"status": "HADIR"
}
Endpoint untuk submit absensi
UPDATE KEAMANAN: Menggunakan Waktu Server untuk validasi dan penyimpanan
"""
try:
data = request.get_json()
id_mahasiswa = request.user_data['id_mahasiswa']
npm = request.user_data['npm']
# Validasi input
required_fields = ['id_jadwal', 'latitude', 'longitude', 'timestamp', 'foto_base64', 'status']
# Validasi input (timestamp dari client kita abaikan untuk logic, tapi tetap dicek keberadaannya gapapa)
required_fields = ['id_jadwal', 'latitude', 'longitude', 'foto_base64', 'status']
for field in required_fields:
if field not in data:
return jsonify({'error': f'Field {field} wajib diisi'}), 400
@ -453,27 +444,21 @@ def submit_absensi():
cursor = connection.cursor(dictionary=True)
# Ambil nama mahasiswa
# 1. Ambil Data Mahasiswa
cursor.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa = %s", (id_mahasiswa,))
mahasiswa = cursor.fetchone()
if not mahasiswa:
cursor.close()
connection.close()
return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404
# Ambil info jadwal & mata kuliah
# 2. Ambil Jadwal
cursor.execute("""
SELECT
j.id_jadwal,
j.jam_mulai,
j.jam_selesai,
m.nama_matkul
SELECT j.id_jadwal, j.jam_mulai, j.jam_selesai, m.nama_matkul
FROM jadwal_kelas j
JOIN mata_kuliah m ON j.id_matkul = m.id_matkul
WHERE j.id_jadwal = %s
""", (data['id_jadwal'],))
jadwal = cursor.fetchone()
if not jadwal:
@ -481,22 +466,29 @@ def submit_absensi():
connection.close()
return jsonify({'error': 'Jadwal tidak ditemukan'}), 404
# Validasi waktu absensi
from datetime import datetime, timedelta
# =========================================================================
# 🛡️ SECURITY FIX: TIME MANIPULATION
# Menggunakan Waktu Server saat ini, BUKAN waktu dari client Android
# =========================================================================
timestamp_absensi = datetime.strptime(data['timestamp'], '%Y-%m-%d %H:%M:%S')
waktu_absensi = timestamp_absensi.time()
waktu_server_sekarang = datetime.now()
# Opsi: Jika server Anda UTC, konversi ke WIB (UTC+7)
# waktu_server_sekarang = datetime.utcnow() + timedelta(hours=7)
jam_sekarang = waktu_server_sekarang.time()
tanggal_sekarang_str = waktu_server_sekarang.strftime('%Y-%m-%d %H:%M:%S')
# Normalisasi jam mulai & selesai dari database
jam_mulai = jadwal['jam_mulai']
jam_selesai = jadwal['jam_selesai']
# CONVERT timedelta ke time jika perlu
# Helper convert timedelta ke time (jika perlu)
if isinstance(jam_mulai, timedelta):
total_seconds = int(jam_mulai.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
jam_mulai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
jam_mulai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
elif isinstance(jam_mulai, str):
jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time()
@ -504,38 +496,38 @@ def submit_absensi():
total_seconds = int(jam_selesai.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
jam_selesai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
jam_selesai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
elif isinstance(jam_selesai, str):
jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time()
if not (jam_mulai <= waktu_absensi <= jam_selesai):
# 3. Validasi Waktu (Pakai Jam Server)
if not (jam_mulai <= jam_sekarang <= jam_selesai):
cursor.close()
connection.close()
return jsonify({
'error': 'Absensi di luar jam kelas',
'error': 'Absensi gagal! Diluar jam kelas (Server Time)',
'detail': {
'jam_mulai': str(jam_mulai),
'jam_selesai': str(jam_selesai),
'waktu_absensi': str(waktu_absensi)
'waktu_server': str(jam_sekarang)
}
}), 400
# Cek apakah sudah absen hari ini untuk jadwal ini
# 4. Cek Double Absen Hari Ini
cursor.execute("""
SELECT COUNT(*) as count
FROM absensi
WHERE id_mahasiswa = %s
AND id_jadwal = %s
AND DATE(timestamp) = DATE(%s)
""", (id_mahasiswa, data['id_jadwal'], data['timestamp']))
""", (id_mahasiswa, data['id_jadwal'], tanggal_sekarang_str))
if cursor.fetchone()['count'] > 0:
cursor.close()
connection.close()
return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400
# Insert absensi ke MySQL
# 5. Insert ke Database (Pakai Waktu Server)
insert_query = """
INSERT INTO absensi (
id_mahasiswa, npm, nama, id_jadwal, mata_kuliah,
@ -543,6 +535,10 @@ def submit_absensi():
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
# Ambil foto (prioritaskan field foto_base64)
foto = data.get('foto_base64') or data.get('photo')
cursor.execute(insert_query, (
id_mahasiswa,
npm,
@ -551,9 +547,9 @@ def submit_absensi():
jadwal['nama_matkul'],
data['latitude'],
data['longitude'],
data['timestamp'],
data.get('photo', data['foto_base64']),
data['foto_base64'],
tanggal_sekarang_str, # <--- PENTING: Simpan waktu server
foto,
foto,
data['status']
))
@ -563,9 +559,8 @@ def submit_absensi():
cursor.close()
connection.close()
# KIRIM KE WEBHOOK N8N
# 6. Kirim ke Webhook N8N (Opsional)
try:
import requests
webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
webhook_payload = {
"npm": npm,
@ -573,13 +568,11 @@ def submit_absensi():
"mata_kuliah": jadwal['nama_matkul'],
"latitude": data['latitude'],
"longitude": data['longitude'],
"timestamp": data['timestamp'],
"photo": data['foto_base64'],
"foto_base64": data['foto_base64'],
"timestamp": tanggal_sekarang_str, # Kirim waktu server
"status": data['status']
}
webhook_response = requests.post(webhook_url, json=webhook_payload, timeout=10)
print(f"✅ Webhook n8n: {webhook_response.status_code}")
# Gunakan try-except timeout agar tidak memblokir response
requests.post(webhook_url, json=webhook_payload, timeout=3)
except Exception as e:
print(f"⚠️ Webhook error: {e}")
@ -588,6 +581,7 @@ def submit_absensi():
'data': {
'id_absensi': id_absensi,
'mata_kuliah': jadwal['nama_matkul'],
'timestamp': tanggal_sekarang_str,
'status': data['status']
}
}), 201