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( val locationPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { granted -> ) { granted ->
if (granted) { if (granted) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(
fusedLocationClient.lastLocation.addOnSuccessListener { location -> context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) { 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 latitude = location.latitude
longitude = location.longitude longitude = location.longitude
val jarak = hitungJarak(location.latitude, location.longitude, AppConstants.KAMPUS_LATITUDE, AppConstants.KAMPUS_LONGITUDE)
val jarak = hitungJarak(
location.latitude,
location.longitude,
AppConstants.KAMPUS_LATITUDE,
AppConstants.KAMPUS_LONGITUDE
)
jarakKeKampus = jarak 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" val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) {
} else { lokasi = "❌ Lokasi tidak tersedia" } "✅ 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 @token_required
def submit_absensi(): def submit_absensi():
""" """
Endpoint untuk submit absensi (UPDATE: dengan validasi jadwal) Endpoint untuk submit absensi
UPDATE KEAMANAN: Menggunakan Waktu Server untuk validasi dan penyimpanan
Request Body:
{
"id_jadwal": 1,
"latitude": -6.223276,
"longitude": 107.009273,
"timestamp": "2026-01-13 14:30:00",
"foto_base64": "base64_string",
"status": "HADIR"
}
""" """
try: try:
data = request.get_json() data = request.get_json()
id_mahasiswa = request.user_data['id_mahasiswa'] id_mahasiswa = request.user_data['id_mahasiswa']
npm = request.user_data['npm'] npm = request.user_data['npm']
# Validasi input # Validasi input (timestamp dari client kita abaikan untuk logic, tapi tetap dicek keberadaannya gapapa)
required_fields = ['id_jadwal', 'latitude', 'longitude', 'timestamp', 'foto_base64', 'status'] required_fields = ['id_jadwal', 'latitude', 'longitude', 'foto_base64', 'status']
for field in required_fields: for field in required_fields:
if field not in data: if field not in data:
return jsonify({'error': f'Field {field} wajib diisi'}), 400 return jsonify({'error': f'Field {field} wajib diisi'}), 400
@ -453,27 +444,21 @@ def submit_absensi():
cursor = connection.cursor(dictionary=True) cursor = connection.cursor(dictionary=True)
# Ambil nama mahasiswa # 1. Ambil Data Mahasiswa
cursor.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa = %s", (id_mahasiswa,)) cursor.execute("SELECT nama FROM mahasiswa WHERE id_mahasiswa = %s", (id_mahasiswa,))
mahasiswa = cursor.fetchone() mahasiswa = cursor.fetchone()
if not mahasiswa: if not mahasiswa:
cursor.close() cursor.close()
connection.close() connection.close()
return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404 return jsonify({'error': 'Mahasiswa tidak ditemukan'}), 404
# Ambil info jadwal & mata kuliah # 2. Ambil Jadwal
cursor.execute(""" cursor.execute("""
SELECT SELECT j.id_jadwal, j.jam_mulai, j.jam_selesai, m.nama_matkul
j.id_jadwal,
j.jam_mulai,
j.jam_selesai,
m.nama_matkul
FROM jadwal_kelas j FROM jadwal_kelas j
JOIN mata_kuliah m ON j.id_matkul = m.id_matkul JOIN mata_kuliah m ON j.id_matkul = m.id_matkul
WHERE j.id_jadwal = %s WHERE j.id_jadwal = %s
""", (data['id_jadwal'],)) """, (data['id_jadwal'],))
jadwal = cursor.fetchone() jadwal = cursor.fetchone()
if not jadwal: if not jadwal:
@ -481,22 +466,29 @@ def submit_absensi():
connection.close() connection.close()
return jsonify({'error': 'Jadwal tidak ditemukan'}), 404 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_server_sekarang = datetime.now()
waktu_absensi = timestamp_absensi.time()
# 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_mulai = jadwal['jam_mulai']
jam_selesai = jadwal['jam_selesai'] jam_selesai = jadwal['jam_selesai']
# CONVERT timedelta ke time jika perlu # Helper convert timedelta ke time (jika perlu)
if isinstance(jam_mulai, timedelta): if isinstance(jam_mulai, timedelta):
total_seconds = int(jam_mulai.total_seconds()) total_seconds = int(jam_mulai.total_seconds())
hours = total_seconds // 3600 hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60 minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60 jam_mulai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
jam_mulai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
elif isinstance(jam_mulai, str): elif isinstance(jam_mulai, str):
jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time() jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time()
@ -504,38 +496,38 @@ def submit_absensi():
total_seconds = int(jam_selesai.total_seconds()) total_seconds = int(jam_selesai.total_seconds())
hours = total_seconds // 3600 hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60 minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60 jam_selesai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
jam_selesai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
elif isinstance(jam_selesai, str): elif isinstance(jam_selesai, str):
jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time() 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() cursor.close()
connection.close() connection.close()
return jsonify({ return jsonify({
'error': 'Absensi di luar jam kelas', 'error': 'Absensi gagal! Diluar jam kelas (Server Time)',
'detail': { 'detail': {
'jam_mulai': str(jam_mulai), 'jam_mulai': str(jam_mulai),
'jam_selesai': str(jam_selesai), 'jam_selesai': str(jam_selesai),
'waktu_absensi': str(waktu_absensi) 'waktu_server': str(jam_sekarang)
} }
}), 400 }), 400
# Cek apakah sudah absen hari ini untuk jadwal ini # 4. Cek Double Absen Hari Ini
cursor.execute(""" cursor.execute("""
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM absensi FROM absensi
WHERE id_mahasiswa = %s WHERE id_mahasiswa = %s
AND id_jadwal = %s AND id_jadwal = %s
AND DATE(timestamp) = DATE(%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: if cursor.fetchone()['count'] > 0:
cursor.close() cursor.close()
connection.close() connection.close()
return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400 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_query = """
INSERT INTO absensi ( INSERT INTO absensi (
id_mahasiswa, npm, nama, id_jadwal, mata_kuliah, 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) 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, ( cursor.execute(insert_query, (
id_mahasiswa, id_mahasiswa,
npm, npm,
@ -551,9 +547,9 @@ def submit_absensi():
jadwal['nama_matkul'], jadwal['nama_matkul'],
data['latitude'], data['latitude'],
data['longitude'], data['longitude'],
data['timestamp'], tanggal_sekarang_str, # <--- PENTING: Simpan waktu server
data.get('photo', data['foto_base64']), foto,
data['foto_base64'], foto,
data['status'] data['status']
)) ))
@ -563,9 +559,8 @@ def submit_absensi():
cursor.close() cursor.close()
connection.close() connection.close()
# KIRIM KE WEBHOOK N8N # 6. Kirim ke Webhook N8N (Opsional)
try: try:
import requests
webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254" webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
webhook_payload = { webhook_payload = {
"npm": npm, "npm": npm,
@ -573,13 +568,11 @@ def submit_absensi():
"mata_kuliah": jadwal['nama_matkul'], "mata_kuliah": jadwal['nama_matkul'],
"latitude": data['latitude'], "latitude": data['latitude'],
"longitude": data['longitude'], "longitude": data['longitude'],
"timestamp": data['timestamp'], "timestamp": tanggal_sekarang_str, # Kirim waktu server
"photo": data['foto_base64'],
"foto_base64": data['foto_base64'],
"status": data['status'] "status": data['status']
} }
webhook_response = requests.post(webhook_url, json=webhook_payload, timeout=10) # Gunakan try-except timeout agar tidak memblokir response
print(f"✅ Webhook n8n: {webhook_response.status_code}") requests.post(webhook_url, json=webhook_payload, timeout=3)
except Exception as e: except Exception as e:
print(f"⚠️ Webhook error: {e}") print(f"⚠️ Webhook error: {e}")
@ -588,6 +581,7 @@ def submit_absensi():
'data': { 'data': {
'id_absensi': id_absensi, 'id_absensi': id_absensi,
'mata_kuliah': jadwal['nama_matkul'], 'mata_kuliah': jadwal['nama_matkul'],
'timestamp': tanggal_sekarang_str,
'status': data['status'] 'status': data['status']
} }
}), 201 }), 201