Compare commits
2
Commits
fcb8869e0a
...
f6db58f299
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6db58f299 | ||
|
|
25d6ef6cdb |
@@ -41,6 +41,7 @@ Saludapp es una aplicación Android moderna enfocada en la salud y el bienestar,
|
|||||||
- `StartScreen.kt`: Pantalla de bienvenida.
|
- `StartScreen.kt`: Pantalla de bienvenida.
|
||||||
- `LoginScreen.kt`: Inicio de sesión con validación de credenciales encriptadas.
|
- `LoginScreen.kt`: Inicio de sesión con validación de credenciales encriptadas.
|
||||||
- `RegisterScreen.kt`: Registro de nuevos usuarios con soporte para nombre completo.
|
- `RegisterScreen.kt`: Registro de nuevos usuarios con soporte para nombre completo.
|
||||||
|
- `HomeScreen.kt`: Dashboard principal con resumen de salud (pasos, sueño, agua, etc.).
|
||||||
- `theme/`: Configuración estética de Material 3.
|
- `theme/`: Configuración estética de Material 3.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies {
|
|||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
|
implementation(libs.androidx.compose.material.icons.extended)
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
|||||||
@@ -4,48 +4,55 @@ import android.os.Bundle
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import com.example.saludapp.ui.HomeScreen
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.example.saludapp.ui.LoginScreen
|
import com.example.saludapp.ui.LoginScreen
|
||||||
import com.example.saludapp.ui.RegisterScreen
|
import com.example.saludapp.ui.RegisterScreen
|
||||||
import com.example.saludapp.ui.StartScreen
|
import com.example.saludapp.ui.StartScreen
|
||||||
import com.example.saludapp.ui.theme.SaludappTheme
|
import com.example.saludapp.ui.theme.SaludappTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actividad principal de la aplicación.
|
||||||
|
* Gestiona el flujo de navegación entre pantallas y el estado de la sesión.
|
||||||
|
*/
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
// Habilitar diseño de borde a borde (edge-to-edge)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
SaludappTheme {
|
SaludappTheme {
|
||||||
|
// Estado para controlar en qué pantalla se encuentra el usuario
|
||||||
var currentScreen by remember { mutableStateOf("start") }
|
var currentScreen by remember { mutableStateOf("start") }
|
||||||
|
// Almacena el nombre del usuario logueado para mostrarlo en el Dashboard
|
||||||
|
var loggedInUserName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
// Enrutador simple basado en el estado 'currentScreen'
|
||||||
when (currentScreen) {
|
when (currentScreen) {
|
||||||
"start" -> StartScreen {
|
"start" -> StartScreen {
|
||||||
currentScreen = "login"
|
currentScreen = "login"
|
||||||
}
|
}
|
||||||
"login" -> LoginScreen(
|
"login" -> LoginScreen(
|
||||||
onLoginSuccess = { currentScreen = "home" },
|
onLoginSuccess = { name ->
|
||||||
|
loggedInUserName = name
|
||||||
|
currentScreen = "home"
|
||||||
|
},
|
||||||
onNavigateToRegister = { currentScreen = "register" }
|
onNavigateToRegister = { currentScreen = "register" }
|
||||||
)
|
)
|
||||||
"register" -> RegisterScreen(
|
"register" -> RegisterScreen(
|
||||||
onRegisterSuccess = { currentScreen = "login" },
|
onRegisterSuccess = { currentScreen = "login" },
|
||||||
onBackToLogin = { currentScreen = "login" }
|
onBackToLogin = { currentScreen = "login" }
|
||||||
)
|
)
|
||||||
"home" -> {
|
"home" -> HomeScreen(
|
||||||
Box(
|
userName = loggedInUserName,
|
||||||
modifier = Modifier.fillMaxSize(),
|
onLogout = {
|
||||||
contentAlignment = Alignment.Center
|
loggedInUserName = ""
|
||||||
) {
|
currentScreen = "start"
|
||||||
Text("Bienvenido a Saludapp")
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,20 @@ import java.sql.Connection
|
|||||||
import java.sql.DriverManager
|
import java.sql.DriverManager
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Objeto encargado de gestionar todas las operaciones con la base de datos MariaDB.
|
||||||
|
* Implementa seguridad mediante hashing de contraseñas y operaciones asíncronas.
|
||||||
|
*/
|
||||||
object DatabaseHelper {
|
object DatabaseHelper {
|
||||||
|
// Configuración de la conexión al servidor externo
|
||||||
private const val URL = "jdbc:mariadb://mariadb.peta9.com:3306/saludapp_db"
|
private const val URL = "jdbc:mariadb://mariadb.peta9.com:3306/saludapp_db"
|
||||||
private const val USER = "saludapp_dba"
|
private const val USER = "saludapp_dba"
|
||||||
private const val PASSWORD = "Pedro@110387"
|
private const val PASSWORD = "Pedro@110387"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encripta la contraseña utilizando el algoritmo SHA-256 para mayor seguridad.
|
||||||
|
* Retorna la representación hexadecimal del hash.
|
||||||
|
*/
|
||||||
private fun hashPassword(password: String): String {
|
private fun hashPassword(password: String): String {
|
||||||
val bytes = password.toByteArray()
|
val bytes = password.toByteArray()
|
||||||
val md = MessageDigest.getInstance("SHA-256")
|
val md = MessageDigest.getInstance("SHA-256")
|
||||||
@@ -19,14 +28,22 @@ object DatabaseHelper {
|
|||||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun validateLogin(user: String, pass: String): Boolean = withContext(Dispatchers.IO) {
|
/**
|
||||||
|
* Valida las credenciales de un usuario.
|
||||||
|
* @param user Nombre de usuario o correo electrónico.
|
||||||
|
* @param pass Contraseña en texto plano (será hasheada antes de comparar).
|
||||||
|
* @return El nombre completo del usuario si tiene éxito, null de lo contrario.
|
||||||
|
*/
|
||||||
|
suspend fun validateLogin(user: String, pass: String): String? = withContext(Dispatchers.IO) {
|
||||||
var connection: Connection? = null
|
var connection: Connection? = null
|
||||||
try {
|
try {
|
||||||
|
// Cargar el driver de MariaDB y establecer conexión
|
||||||
Class.forName("org.mariadb.jdbc.Driver")
|
Class.forName("org.mariadb.jdbc.Driver")
|
||||||
connection = DriverManager.getConnection(URL, USER, PASSWORD)
|
connection = DriverManager.getConnection(URL, USER, PASSWORD)
|
||||||
|
|
||||||
val hashedPass = hashPassword(pass)
|
val hashedPass = hashPassword(pass)
|
||||||
val query = "SELECT COUNT(*) FROM usuarios WHERE (username = ? OR email = ?) AND password = ?"
|
// Consulta preparada para evitar ataques de inyección SQL
|
||||||
|
val query = "SELECT nombre_completo FROM usuarios WHERE (username = ? OR email = ?) AND password = ?"
|
||||||
connection.prepareStatement(query).use { statement ->
|
connection.prepareStatement(query).use { statement ->
|
||||||
statement.setString(1, user)
|
statement.setString(1, user)
|
||||||
statement.setString(2, user)
|
statement.setString(2, user)
|
||||||
@@ -34,21 +51,31 @@ object DatabaseHelper {
|
|||||||
|
|
||||||
val resultSet = statement.executeQuery()
|
val resultSet = statement.executeQuery()
|
||||||
if (resultSet.next()) {
|
if (resultSet.next()) {
|
||||||
return@withContext resultSet.getInt(1) > 0
|
// Retorna el nombre real del usuario guardado en la DB
|
||||||
|
return@withContext resultSet.getString("nombre_completo") ?: user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} finally {
|
} finally {
|
||||||
|
// Asegurar que la conexión se cierra siempre
|
||||||
try {
|
try {
|
||||||
connection?.close()
|
connection?.close()
|
||||||
} catch (e: SQLException) {
|
} catch (e: SQLException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@withContext false
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra un nuevo usuario en la base de datos.
|
||||||
|
* @param user Nombre de usuario único.
|
||||||
|
* @param email Correo electrónico único.
|
||||||
|
* @param pass Contraseña en texto plano (se guardará como hash SHA-256).
|
||||||
|
* @param fullName Nombre completo del usuario.
|
||||||
|
* @return true si el registro fue exitoso, false en caso contrario.
|
||||||
|
*/
|
||||||
suspend fun registerUser(user: String, email: String, pass: String, fullName: String): Boolean = withContext(Dispatchers.IO) {
|
suspend fun registerUser(user: String, email: String, pass: String, fullName: String): Boolean = withContext(Dispatchers.IO) {
|
||||||
var connection: Connection? = null
|
var connection: Connection? = null
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.example.saludapp.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.DirectionsRun
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pantalla principal (Dashboard) que se muestra tras un inicio de sesión exitoso.
|
||||||
|
* @param userName Nombre del usuario para el mensaje de bienvenida.
|
||||||
|
* @param onLogout Acción a ejecutar cuando el usuario decide cerrar sesión.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun HomeScreen(userName: String, onLogout: () -> Unit) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Saludapp", fontWeight = FontWeight.Bold) },
|
||||||
|
actions = {
|
||||||
|
// Botón para cerrar la sesión actual
|
||||||
|
IconButton(onClick = onLogout) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ExitToApp, contentDescription = "Cerrar sesión")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Mensaje de bienvenida personalizado
|
||||||
|
Text(
|
||||||
|
text = "Hola, $userName 👋",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Aquí tienes tu resumen de hoy",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Cuadrícula de tarjetas con estadísticas de salud
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
StatCard("Pasos", "8,432", Icons.AutoMirrored.Filled.DirectionsRun, MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
StatCard("Sueño", "7h 20m", Icons.Default.Bedtime, MaterialTheme.colorScheme.secondary)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
StatCard("Agua", "1.5L", Icons.Default.LocalDrink, MaterialTheme.colorScheme.tertiary)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
StatCard("Calorías", "1,200 kcal", Icons.Default.Whatshot, MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
StatCard("Corazón", "72 bpm", Icons.Default.Favorite, MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
StatCard("Peso", "70 kg", Icons.Default.MonitorWeight, MaterialTheme.colorScheme.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Componente reutilizable para mostrar una tarjeta de estadística.
|
||||||
|
* @param title Título del dato (ej. "Agua").
|
||||||
|
* @param value Valor actual (ej. "1.5L").
|
||||||
|
* @param icon Icono representativo.
|
||||||
|
* @param color Color temático para la tarjeta y el icono.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun StatCard(title: String, value: String, icon: ImageVector, color: androidx.compose.ui.graphics.Color) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(140.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = color,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = value,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import com.example.saludapp.data.DatabaseHelper
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(onLoginSuccess: () -> Unit, onNavigateToRegister: () -> Unit) {
|
fun LoginScreen(onLoginSuccess: (String) -> Unit, onNavigateToRegister: () -> Unit) {
|
||||||
var email by remember { mutableStateOf("") }
|
var email by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var isLoading by remember { mutableStateOf(false) }
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
@@ -73,10 +73,10 @@ fun LoginScreen(onLoginSuccess: () -> Unit, onNavigateToRegister: () -> Unit) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
val success = DatabaseHelper.validateLogin(email, password)
|
val userName = DatabaseHelper.validateLogin(email, password)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
if (success) {
|
if (userName != null) {
|
||||||
onLoginSuccess()
|
onLoginSuccess(userName)
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "Usuario o contraseña incorrectos"
|
errorMessage = "Usuario o contraseña incorrectos"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
|||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user