added passwort for login and download, token-system

This commit is contained in:
oxidiert
2025-08-18 16:27:11 +02:00
parent 148af18496
commit a1736d0241
6 changed files with 192 additions and 85 deletions

View File

@ -16,21 +16,24 @@ import javax.crypto.spec.SecretKeySpec
object DatabaseDownloader {
private const val DB_NAME = "questionnaire_database"
private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php?token=$API_TOKEN"
private const val SERVER_DOWNLOAD_URL = "http://49.13.157.44/downloadFull.php"
// AES-256 Key (muss exakt 32 Bytes lang sein)
private const val AES_KEY = "12345678901234567890123456789012"
private val client = OkHttpClient()
fun downloadAndReplaceDatabase(context: Context) {
/**
* Startet den Download und Austausch der DB, benötigt gültiges Token
*/
fun downloadAndReplaceDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.d("DOWNLOAD", "Download gestartet: $SERVER_DOWNLOAD_URL")
val request = Request.Builder()
.url(SERVER_DOWNLOAD_URL)
.header("Authorization", "Bearer $token")
.build()
val response = client.newCall(request).execute()

View File

@ -20,14 +20,32 @@ import kotlin.system.exitProcess
object DatabaseUploader {
private const val DB_NAME = "questionnaire_database"
// TODO entferne uploadDeltaTest2.php
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest3.php"
private const val SERVER_DELTA_URL = "http://49.13.157.44/uploadDeltaTest4.php"
private const val SERVER_CHECK_URL = "http://49.13.157.44/checkDatabaseExists.php"
private const val API_TOKEN = "MEIN_SUPER_GEHEIMES_TOKEN_12345"
private val client = OkHttpClient()
fun uploadDatabase(context: Context) {
/**
* Startet den Upload mit Login über LoginManager.
* @param context Android Context
* @param password Vom User eingegebenes Passwort
*/
fun uploadDatabaseWithLogin(context: Context, password: String) {
LoginManager.loginUser(context, password,
onSuccess = { token ->
Log.d("UPLOAD", "Login erfolgreich, Token erhalten")
uploadDatabase(context, token)
},
onError = { errorMsg ->
Log.e("UPLOAD", "Login fehlgeschlagen: $errorMsg")
}
)
}
/**
* Interner Upload, benötigt gültiges Token
*/
private fun uploadDatabase(context: Context, token: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val dbFile = context.getDatabasePath(DB_NAME)
@ -45,9 +63,7 @@ object DatabaseUploader {
)
db.rawQuery("PRAGMA wal_checkpoint(FULL);", null).use { cursor ->
if (cursor.moveToFirst()) {
try {
Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}")
} catch (_: Exception) {}
try { Log.d("UPLOAD", "WAL-Checkpoint result: ${cursor.getInt(0)}") } catch (_: Exception) {}
}
}
db.close()
@ -59,12 +75,12 @@ object DatabaseUploader {
val exists = checkDatabaseExists()
if (exists) {
Log.d("UPLOAD", "Server-Datenbank vorhanden → Delta-Upload")
uploadPseudoDelta(context, dbFile)
} else {
Log.d("UPLOAD", "Keine Server-Datenbank → Delta-Upload")
uploadPseudoDelta(context, dbFile)
}
uploadPseudoDelta(context, dbFile, token)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler beim Hochladen der DB", e)
}
@ -97,15 +113,7 @@ object DatabaseUploader {
}
}
/**
* Wichtig: Diese Funktion wurde erweitert, sodass:
* - die DB als JSON in eine temporäre Datei geschrieben wird,
* - diese JSON-Datei AES-verschlüsselt wird (mit AES256Helper.encryptFile),
* - die verschlüsselte Datei als Multipart 'file' an den Server gesendet wird.
*
* (Funktionalität: gleiche Signatur wie vorher behalten)
*/
private fun uploadPseudoDelta(context: Context, file: File) {
private fun uploadPseudoDelta(context: Context, file: File, token: String) {
try {
val db = SQLiteDatabase.openDatabase(file.absolutePath, null, SQLiteDatabase.OPEN_READONLY)
@ -125,25 +133,21 @@ object DatabaseUploader {
db.close()
// Schreibe JSON in temporäre Datei
val tmpJson = File(context.cacheDir, "payload.json")
tmpJson.writeText(data.toString())
// Verschlüssele JSON -> tmpEnc
val tmpEnc = File(context.cacheDir, "payload.enc")
try {
AES256Helper.encryptFile(tmpJson, tmpEnc)
} catch (e: Exception) {
Log.e("UPLOAD", "Fehler bei der Verschlüsselung der JSON-Datei", e)
// cleanup
tmpJson.delete()
return
}
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("token", API_TOKEN)
// Datei-Feld "file" mit verschlüsselter Payload
.addFormDataPart("token", token) // Token vom Login
.addFormDataPart(
"file",
"payload.enc",
@ -159,31 +163,19 @@ object DatabaseUploader {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("UPLOAD", "Delta-Upload fehlgeschlagen: ${e.message}")
// cleanup
tmpJson.delete()
tmpEnc.delete()
}
override fun onResponse(call: Call, response: Response) {
val body = try {
response.body?.string() ?: "Keine Response"
} catch (e: Exception) {
val body = try { response.body?.string() ?: "Keine Response" } catch (e: Exception) {
"Fehler beim Lesen der Response: ${e.message}"
}
if (response.isSuccessful) {
Log.d("UPLOAD", "Delta-Upload erfolgreich: $body")
// Lösche Hauptdatenbank
if (file.delete()) {
Log.d("UPLOAD", "Lokale DB gelöscht.")
} else {
Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
}
// Lösche Journal-Datei
if (file.delete()) Log.d("UPLOAD", "Lokale DB gelöscht.") else Log.e("UPLOAD", "Löschen der lokalen DB fehlgeschlagen.")
val journalFile = File(file.parent, file.name + "-journal")
if (journalFile.exists() && journalFile.delete()) {
Log.d("UPLOAD", "Journal-Datei gelöscht.")
}
// cleanup temp files
if (journalFile.exists() && journalFile.delete()) Log.d("UPLOAD", "Journal-Datei gelöscht.")
tmpJson.delete()
tmpEnc.delete()
exitProcess(0)

View File

@ -69,10 +69,9 @@ class HandlerClientCoachCode(
val clientCode = clientCodeField.text.toString()
val coachCode = coachCodeField.text.toString()
// Prüfen, ob die Datenbank-Dateien vor dem Klick existieren
val dbFile = layout.context.getDatabasePath("questionnaire_database")
val dbJournalFile = layout.context.getDatabasePath("questionnaire_database-journal")
val dbExisted = dbFile.exists() || dbJournalFile.exists()
// Prüfen, ob die DB-Datei vor dem Zugriff existiert
val dbPath = layout.context.getDatabasePath("questionnaire_database")
val dbExistedBefore = dbPath.exists()
// Check if client code already exists asynchronously
CoroutineScope(Dispatchers.IO).launch {
@ -86,19 +85,21 @@ class HandlerClientCoachCode(
} else {
// Either no existing client or re-using previous code
saveAnswers(clientCode, coachCode)
// Datenbank-Dateien löschen, wenn sie vorher NICHT existierten
if (!dbExisted) {
dbFile.delete()
dbJournalFile.delete()
}
goToNextQuestion()
// Lösche DB-Dateien nur, wenn sie vorher nicht existierten
if (!dbExistedBefore) {
MyApp.database.close()
dbPath.delete()
val journalFile = layout.context.getDatabasePath("questionnaire_database-journal")
journalFile.delete()
}
}
}
}
}
// Handle Previous button click
private fun onPreviousClicked(clientCodeField: EditText, coachCodeField: EditText) {
val clientCode = clientCodeField.text.toString()

View File

@ -13,7 +13,7 @@ import android.util.Log
import com.dano.test1.data.CompletedQuestionnaire
import java.io.File
var INTEGRATION_INDEX_POINTS: Int? = null
var RHS_POINTS: Int? = null
class HandlerOpeningScreen(private val activity: MainActivity) {
@ -342,13 +342,12 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
MyApp.database.completedQuestionnaireDao().getAllForClient(clientCode)
}
// fülle buttonPoints & INTEGRATION_INDEX_POINTS
buttonPoints.clear()
for (entry in completedEntries) {
if (entry.isDone) {
buttonPoints[entry.questionnaireId] = entry.sumPoints ?: 0
if (entry.questionnaireId.contains("questionnaire_3_integration_index", ignoreCase = true)) {
INTEGRATION_INDEX_POINTS = entry.sumPoints
if (entry.questionnaireId.contains("questionnaire_2_rhs", ignoreCase = true)) {
RHS_POINTS = entry.sumPoints
}
}
}
@ -682,39 +681,79 @@ class HandlerOpeningScreen(private val activity: MainActivity) {
uploadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
if (clientCode.isBlank()) {
val message = LanguageManager.getText(languageID, "please_client_code")
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
GlobalValues.LAST_CLIENT_CODE = clientCode
Toast.makeText(activity, "Datenbank wird hochgeladen...", Toast.LENGTH_SHORT).show()
DatabaseUploader.uploadDatabase(activity)
// Passwort-Eingabe-Popup
val input = EditText(activity).apply {
hint = "Server-Passwort"
}
android.app.AlertDialog.Builder(activity)
.setTitle("Login erforderlich")
.setView(input)
.setPositiveButton("OK") { _, _ ->
val password = input.text.toString()
if (password.isNotBlank()) {
Toast.makeText(activity, "Login wird überprüft...", Toast.LENGTH_SHORT).show()
// Login + Upload starten
DatabaseUploader.uploadDatabaseWithLogin(activity, password)
} else {
Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("Abbrechen", null)
.show()
}
}
// --- Füge diese Funktion in deine Klasse ein ---
private fun isDatabasePopulated(): Boolean {
return try {
val db = MyApp.database.openHelper.readableDatabase
val cursor = db.query(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'room_master_table'"
)
cursor.use { it.count > 0 }
} catch (e: Exception) {
false
}
}
private fun setupDownloadButton() {
downloadButton.text = "Download"
downloadButton.setOnClickListener {
val clientCode = editText.text.toString().trim()
GlobalValues.LAST_CLIENT_CODE = clientCode
Toast.makeText(activity, "Datenbank wird heruntergeladen...", Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity)
updateMainButtonsState(true)
// Eingabe-Popup für Passwort anzeigen
val input = EditText(activity).apply {
hint = "Server-Passwort"
}
android.app.AlertDialog.Builder(activity)
.setTitle("Login erforderlich")
.setView(input)
.setPositiveButton("OK") { _, _ ->
val password = input.text.toString()
if (password.isNotBlank()) {
// Login starten
LoginManager.loginUser(
context = activity,
password = password,
onSuccess = { token ->
Toast.makeText(activity, "Login erfolgreich", Toast.LENGTH_SHORT).show()
DatabaseDownloader.downloadAndReplaceDatabase(activity, token)
updateMainButtonsState(true)
},
onError = { error ->
Toast.makeText(activity, error, Toast.LENGTH_LONG).show()
}
)
} else {
Toast.makeText(activity, "Bitte Passwort eingeben", Toast.LENGTH_SHORT).show()
}
}
.setNegativeButton("Abbrechen", null)
.show()
}
}
private fun updateMainButtonsState(isDatabaseAvailable: Boolean) {
val buttons = listOf(buttonLoad, saveButton, editButton)
buttons.forEach { button ->

View File

@ -17,7 +17,7 @@ object LanguageManager {
}
private fun injectDynamicValues(text: String): String {
val points = INTEGRATION_INDEX_POINTS ?: 0
val points = RHS_POINTS ?: 0
val color = when (points) {
in 1..12 -> "#4CAF50" // Grün
in 13..36 -> "#FFEB3B" // Gelb
@ -26,7 +26,7 @@ object LanguageManager {
}
val coloredPoints = "<b><font color='$color'>$points</font></b>"
return text.replace("INTEGRATION_INDEX_POINTS", coloredPoints)
return text.replace("RHS_POINTS", coloredPoints)
}
// Sprachdatenbank: Map<Sprachcode, Map<Text-ID, Text>>
@ -285,7 +285,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Bitte wählen Sie eine Antwort pro Reihe aus!",
"no_next_question_defined" to "Keine Weiterleitungsseite definiert",
"date_consultation_health_interview_result" to "Datum Beratungsgespräch (zum Ergebnis Gesundheitsinterview grün/gelb/rot)",
"consultation_decision" to "Beratungsentscheidung (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Beratungsentscheidung (RHS_POINTS)",
"consent_conversation_in_6_months" to "Einverständnis Gespräch in 6 Monaten:",
"participation_in_coaching" to "Teilnahme am Coaching",
"decision_after_reflection_period" to "Entscheidung am .............. (Datum) nach Bedenkzeit",
@ -580,7 +580,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Please select one answer per row!",
"no_next_question_defined" to "No forwarding page defined",
"date_consultation_health_interview_result" to "Date of counseling interview (health interview result green/yellow/red)",
"consultation_decision" to "Counseling decision (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Counseling decision (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consent for conversation in 6 months:",
"participation_in_coaching" to "Participation in coaching",
"decision_after_reflection_period" to "Decision on .............. (date) after reflection period",
@ -878,7 +878,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Veuillez sélectionner une réponse par ligne !",
"no_next_question_defined" to "Aucune page de redirection définie",
"date_consultation_health_interview_result" to "Date de lentretien de conseil (résultat de lentretien santé vert/jaune/rouge)",
"consultation_decision" to "Décision de conseil (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Décision de conseil (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consentement pour entretien dans 6 mois :",
"participation_in_coaching" to "Participation au coaching",
"decision_after_reflection_period" to "Décision le .............. (date) après période de réflexion",
@ -1172,7 +1172,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Пожалуйста, выберите один ответ в каждой строке!",
"no_next_question_defined" to "Следующая страница не определена",
"date_consultation_health_interview_result" to "Дата консультации (результат медицинского интервью: зеленый/желтый/красный)",
"consultation_decision" to "Решение по консультации (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Решение по консультации (RHS_POINTS)",
"consent_conversation_in_6_months" to "Согласие на разговор через 6 месяцев:",
"participation_in_coaching" to "Участие в коучинге",
"decision_after_reflection_period" to "Решение от .............. (дата) после периода раздумий",
@ -1470,7 +1470,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Будь ласка, оберіть по одній відповіді в кожному рядку!",
"no_next_question_defined" to "Наступна сторінка не визначена",
"date_consultation_health_interview_result" to "Дата консультації (результат медичного інтерв’ю зелений/жовтий/червоний)",
"consultation_decision" to "Рішення консультації (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Рішення консультації (RHS_POINTS)",
"consent_conversation_in_6_months" to "Згода на розмову через 6 місяців:",
"participation_in_coaching" to "Участь у коучингу",
"decision_after_reflection_period" to "Рішення .............. (дата) після періоду роздумів",
@ -1768,7 +1768,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Lütfen her satır için bir cevap seçin!",
"no_next_question_defined" to "Bir yönlendirme sayfası tanımlanmadı",
"date_consultation_health_interview_result" to "Danışma görüşmesi tarihi (sağlık görüşmesi sonucu yeşil/sarı/kırmızı)",
"consultation_decision" to "Danışma kararı (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Danışma kararı (RHS_POINTS)",
"consent_conversation_in_6_months" to "6 ay içinde görüşme onayı:",
"participation_in_coaching" to "Koçluğa katılım",
"decision_after_reflection_period" to "Karar .............. (tarih) tarihinde düşünme süresinden sonra verildi",
@ -2066,7 +2066,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Proszę wybrać jedną odpowiedź w każdym wierszu!",
"no_next_question_defined" to "Nie zdefiniowano strony przekierowania",
"date_consultation_health_interview_result" to "Data rozmowy doradczej (wynik wywiadu zdrowotnego: zielony/żółty/czerwony)",
"consultation_decision" to "Decyzja doradcza (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Decyzja doradcza (RHS_POINTS)",
"consent_conversation_in_6_months" to "Zgoda na rozmowę za 6 miesięcy:",
"participation_in_coaching" to "Udział w coachingu",
"decision_after_reflection_period" to "Decyzja dnia .............. (data) po czasie do namysłu",
@ -2662,7 +2662,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Vă rugăm să selectați un răspuns pe rând!",
"no_next_question_defined" to "Nu este definită o pagină de redirecționare",
"date_consultation_health_interview_result" to "Data consilierii (rezultatul interviului de sănătate verde/galben/roșu)",
"consultation_decision" to "Decizia consilierii (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Decizia consilierii (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consimțământ discuție peste 6 luni:",
"participation_in_coaching" to "Participare la coaching",
"decision_after_reflection_period" to "Decizie la .............. (dată) după perioada de reflecție",
@ -2960,7 +2960,7 @@ object LanguageManager {
"select_one_answer_per_row" to "Por favor, seleccione una respuesta por fila.",
"no_next_question_defined" to "No se definió ninguna pregunta de continuación",
"date_consultation_health_interview_result" to "Fecha de la entrevista de orientación (sobre el resultado de la entrevista de salud: verde/amarillo/rojo)",
"consultation_decision" to "Decisión de orientación (INTEGRATION_INDEX_POINTS)",
"consultation_decision" to "Decisión de orientación (RHS_POINTS)",
"consent_conversation_in_6_months" to "Consentimiento para entrevista en 6 meses:",
"participation_in_coaching" to "Participación en el coaching",
"decision_after_reflection_period" to "Decisión tomada el .............. (fecha) después del período de reflexión",

View File

@ -0,0 +1,72 @@
package com.dano.test1
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
object LoginManager {
private const val SERVER_LOGIN_URL = "http://49.13.157.44/login.php"
private val client = OkHttpClient()
/**
* Startet den Login-Prozess.
*
* @param context Android Context
* @param password Vom User eingegebenes Passwort
* @param onSuccess Callback mit dem Token wenn Login erfolgreich
* @param onError Callback mit Fehlermeldung
*/
fun loginUser(
context: Context,
password: String,
onSuccess: (String) -> Unit,
onError: (String) -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
val requestBody = """{"password":"$password"}"""
.toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url(SERVER_LOGIN_URL)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
val responseText = response.body?.string()
if (response.isSuccessful && responseText != null) {
val json = JSONObject(responseText)
if (json.getBoolean("success")) {
val token = json.getString("token")
withContext(Dispatchers.Main) {
onSuccess(token)
}
} else {
withContext(Dispatchers.Main) {
onError("Login fehlgeschlagen")
}
}
} else {
withContext(Dispatchers.Main) {
onError("Fehler beim Login (${response.code})")
}
}
} catch (e: Exception) {
Log.e("LOGIN", "Exception beim Login", e)
withContext(Dispatchers.Main) {
onError("Exception: ${e.message}")
}
}
}
}
}