Compare commits
No commits in common. "14c4c19a4f0b56fabb25d2cd44d5e395224ce7c6" and "085ea807c9bcca63d0e303e8914c6040df970e2c" have entirely different histories.
14c4c19a4f
...
085ea807c9
@ -1874,71 +1874,26 @@ 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 (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
|
||||
)
|
||||
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)"
|
||||
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" }
|
||||
}
|
||||
}
|
||||
.addOnFailureListener {
|
||||
lokasi = "❌ Gagal mengambil lokasi"
|
||||
errorMessage = "Gagal mengambil lokasi: ${it.message}"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMessage = "⚠️ Izin lokasi ditolak. Aplikasi tidak dapat digunakan."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB |
@ -424,16 +424,25 @@ def get_profile():
|
||||
@token_required
|
||||
def submit_absensi():
|
||||
"""
|
||||
Endpoint untuk submit absensi
|
||||
UPDATE KEAMANAN: Menggunakan Waktu Server untuk validasi dan penyimpanan
|
||||
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"
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
id_mahasiswa = request.user_data['id_mahasiswa']
|
||||
npm = request.user_data['npm']
|
||||
|
||||
# Validasi input (timestamp dari client kita abaikan untuk logic, tapi tetap dicek keberadaannya gapapa)
|
||||
required_fields = ['id_jadwal', 'latitude', 'longitude', 'foto_base64', 'status']
|
||||
# Validasi input
|
||||
required_fields = ['id_jadwal', 'latitude', 'longitude', 'timestamp', 'foto_base64', 'status']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({'error': f'Field {field} wajib diisi'}), 400
|
||||
@ -444,21 +453,27 @@ def submit_absensi():
|
||||
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
# 1. Ambil Data Mahasiswa
|
||||
# Ambil nama 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
|
||||
|
||||
# 2. Ambil Jadwal
|
||||
# Ambil info jadwal & mata kuliah
|
||||
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:
|
||||
@ -466,29 +481,22 @@ def submit_absensi():
|
||||
connection.close()
|
||||
return jsonify({'error': 'Jadwal tidak ditemukan'}), 404
|
||||
|
||||
# =========================================================================
|
||||
# 🛡️ SECURITY FIX: TIME MANIPULATION
|
||||
# Menggunakan Waktu Server saat ini, BUKAN waktu dari client Android
|
||||
# =========================================================================
|
||||
# Validasi waktu absensi
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
waktu_server_sekarang = datetime.now()
|
||||
timestamp_absensi = datetime.strptime(data['timestamp'], '%Y-%m-%d %H:%M:%S')
|
||||
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_selesai = jadwal['jam_selesai']
|
||||
|
||||
# Helper convert timedelta ke time (jika perlu)
|
||||
# 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
|
||||
jam_mulai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
|
||||
seconds = total_seconds % 60
|
||||
jam_mulai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
|
||||
elif isinstance(jam_mulai, str):
|
||||
jam_mulai = datetime.strptime(jam_mulai, '%H:%M:%S').time()
|
||||
|
||||
@ -496,38 +504,38 @@ def submit_absensi():
|
||||
total_seconds = int(jam_selesai.total_seconds())
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
jam_selesai = datetime.strptime(f"{hours}:{minutes}:00", '%H:%M:%S').time()
|
||||
seconds = total_seconds % 60
|
||||
jam_selesai = datetime.strptime(f"{hours}:{minutes}:{seconds}", '%H:%M:%S').time()
|
||||
elif isinstance(jam_selesai, str):
|
||||
jam_selesai = datetime.strptime(jam_selesai, '%H:%M:%S').time()
|
||||
|
||||
# 3. Validasi Waktu (Pakai Jam Server)
|
||||
if not (jam_mulai <= jam_sekarang <= jam_selesai):
|
||||
if not (jam_mulai <= waktu_absensi <= jam_selesai):
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return jsonify({
|
||||
'error': 'Absensi gagal! Diluar jam kelas (Server Time)',
|
||||
'error': 'Absensi di luar jam kelas',
|
||||
'detail': {
|
||||
'jam_mulai': str(jam_mulai),
|
||||
'jam_selesai': str(jam_selesai),
|
||||
'waktu_server': str(jam_sekarang)
|
||||
'waktu_absensi': str(waktu_absensi)
|
||||
}
|
||||
}), 400
|
||||
|
||||
# 4. Cek Double Absen Hari Ini
|
||||
# Cek apakah sudah absen hari ini untuk jadwal 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'], tanggal_sekarang_str))
|
||||
""", (id_mahasiswa, data['id_jadwal'], data['timestamp']))
|
||||
|
||||
if cursor.fetchone()['count'] > 0:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return jsonify({'error': 'Anda sudah absen untuk kelas ini hari ini'}), 400
|
||||
|
||||
# 5. Insert ke Database (Pakai Waktu Server)
|
||||
# Insert absensi ke MySQL
|
||||
insert_query = """
|
||||
INSERT INTO absensi (
|
||||
id_mahasiswa, npm, nama, id_jadwal, mata_kuliah,
|
||||
@ -535,10 +543,6 @@ 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,
|
||||
@ -547,9 +551,9 @@ def submit_absensi():
|
||||
jadwal['nama_matkul'],
|
||||
data['latitude'],
|
||||
data['longitude'],
|
||||
tanggal_sekarang_str, # <--- PENTING: Simpan waktu server
|
||||
foto,
|
||||
foto,
|
||||
data['timestamp'],
|
||||
data.get('photo', data['foto_base64']),
|
||||
data['foto_base64'],
|
||||
data['status']
|
||||
))
|
||||
|
||||
@ -559,8 +563,9 @@ def submit_absensi():
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
# 6. Kirim ke Webhook N8N (Opsional)
|
||||
# KIRIM KE WEBHOOK N8N
|
||||
try:
|
||||
import requests
|
||||
webhook_url = "https://n8n.lab.ubharajaya.ac.id/webhook/23c6993d-1792-48fb-ad1c-ffc78a3e6254"
|
||||
webhook_payload = {
|
||||
"npm": npm,
|
||||
@ -568,11 +573,13 @@ def submit_absensi():
|
||||
"mata_kuliah": jadwal['nama_matkul'],
|
||||
"latitude": data['latitude'],
|
||||
"longitude": data['longitude'],
|
||||
"timestamp": tanggal_sekarang_str, # Kirim waktu server
|
||||
"timestamp": data['timestamp'],
|
||||
"photo": data['foto_base64'],
|
||||
"foto_base64": data['foto_base64'],
|
||||
"status": data['status']
|
||||
}
|
||||
# Gunakan try-except timeout agar tidak memblokir response
|
||||
requests.post(webhook_url, json=webhook_payload, timeout=3)
|
||||
webhook_response = requests.post(webhook_url, json=webhook_payload, timeout=10)
|
||||
print(f"✅ Webhook n8n: {webhook_response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Webhook error: {e}")
|
||||
|
||||
@ -581,7 +588,6 @@ def submit_absensi():
|
||||
'data': {
|
||||
'id_absensi': id_absensi,
|
||||
'mata_kuliah': jadwal['nama_matkul'],
|
||||
'timestamp': tanggal_sekarang_str,
|
||||
'status': data['status']
|
||||
}
|
||||
}), 201
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user