Compare commits
2 Commits
085ea807c9
...
14c4c19a4f
| Author | SHA1 | Date | |
|---|---|---|---|
| 14c4c19a4f | |||
| 8e5e82c3d8 |
@ -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,
|
||||||
if (location != null) {
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
latitude = location.latitude
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
longitude = location.longitude
|
) {
|
||||||
val jarak = hitungJarak(location.latitude, location.longitude, AppConstants.KAMPUS_LATITUDE, AppConstants.KAMPUS_LONGITUDE)
|
fusedLocationClient.lastLocation
|
||||||
jarakKeKampus = jarak
|
.addOnSuccessListener { location ->
|
||||||
val statusLokasi = if (jarak <= AppConstants.RADIUS_METER) "✅ DI DALAM AREA" else "❌ DI LUAR AREA"
|
if (location != null) {
|
||||||
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" }
|
// 🛡️ 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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/logo_ubhara.png
Normal file
BIN
app/src/main/res/drawable/logo_ubhara.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user