package iotmonitor import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.time.Duration /** * Logique réseau minimale. Réutilisable telle quelle dans une MainActivity. * * Règles de sécurité (modules 03/04) : * - le secret de device passe par en-tête, jamais loggé ; * - on n'imprime que l'identité et le résultat ; * - les erreurs sont remontées sans détail d'implémentation. */ class BackendClient( private val baseUrl: String, private val http: HttpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(), ) { /** Envoie une mesure en tant que device. Retourne le code de statut HTTP. */ fun ingest(deviceId: String, deviceSecret: String, measure: Measure): Int { val body = buildIngestBody(measure) val request = HttpRequest.newBuilder() .uri(URI.create("$baseUrl/api/ingest")) .header("Content-Type", "application/json") .header("X-Device-ID", deviceId) .header("X-Device-Secret", deviceSecret) .POST(HttpRequest.BodyPublishers.ofString(body)) .build() val response = http.send(request, HttpResponse.BodyHandlers.ofString()) // Le secret n'est jamais imprimé. println("[ingest] device=$deviceId -> HTTP ${response.statusCode()}") return response.statusCode() } /** * Liste les mesures d'un device en tant qu'utilisateur. * L'autorisation est décidée par le backend (module 03) : 403 => refus. */ fun listMeasures(userToken: String, deviceId: String): List { val request = HttpRequest.newBuilder() .uri(URI.create("$baseUrl/api/measures?device_id=$deviceId")) .header("Authorization", "Bearer $userToken") .GET() .build() val response = http.send(request, HttpResponse.BodyHandlers.ofString()) println("[measures] device=$deviceId -> HTTP ${response.statusCode()}") if (response.statusCode() != 200) { return emptyList() } return parseMeasures(response.body()) } private fun buildIngestBody(measure: Measure): String = """{"device_id":"${measure.device_id}","timestamp":"${measure.timestamp}", "light_value":${measure.light_value}}""" private fun parseMeasures(body: String): List { // Parse très simple, sans dépendance JSON : on extrait chaque objet // entre accolades (ordre des champs indifférent), puis les valeurs // par clé. Suffisant pour le format minimal du backend. val objectRegex = Regex("""\{[^{}]*\}""") val fieldRegex = Regex(""""([a-z_]+)":"?([^",\}]+)"?""") return objectRegex.findAll(body).mapNotNull { objMatch -> val fields = fieldRegex.findAll(objMatch.value) .associate { m -> m.groupValues[1] to m.groupValues[2] } val deviceId = fields["device_id"] ?: return@mapNotNull null val timestamp = fields["timestamp"] ?: return@mapNotNull null val light = fields["light_value"]?.toLongOrNull() ?: return@mapNotNull null Measure(device_id = deviceId, timestamp = timestamp, light_value = light) }.toList() } }