From 82aa70cf99f870b09fd53c97d2e214e684f9069d Mon Sep 17 00:00:00 2001 From: Intisar Osman Date: Sat, 25 Nov 2023 00:35:31 -0800 Subject: [PATCH 1/3] Added functionality to the delete button --- .../data/network/HttpRoutes.kt | 1 + .../data/network/auth/DeleteService.kt | 42 ++++++++++++++++ .../data/network/auth/DeleteServiceImpl.kt | 48 +++++++++++++++++++ .../network/dto/auth_dto/DeleteRequest.kt | 13 +++++ .../network/dto/auth_dto/DeleteResponse.kt | 10 ++++ .../screen/IndividualProductUpdate.kt | 30 +++++++++++- app/src/main/res/values/strings.xml | 3 ++ 7 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteService.kt create mode 100644 app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt create mode 100644 app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteRequest.kt create mode 100644 app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteResponse.kt diff --git a/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt b/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt index 04af3c94..29892cac 100644 --- a/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt +++ b/app/src/main/java/com/example/belindas_closet/data/network/HttpRoutes.kt @@ -5,6 +5,7 @@ object HttpRoutes { const val PRODUCTS = "$BASE_URL/products" const val PRODUCT = "$BASE_URL/products/{id}" const val ARCHIVE = "$BASE_URL/products/archive" + const val DELETE = "$BASE_URL/products/remove" const val LOGIN = "$BASE_URL/auth/login" const val FORGOT_PASSWORD = "$BASE_URL/auth/forgot-password" const val SIGNUP = "$BASE_URL/auth/signup" diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteService.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteService.kt new file mode 100644 index 00000000..6c1bf4d0 --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteService.kt @@ -0,0 +1,42 @@ +package com.example.belindas_closet.data.network.auth + +import com.example.belindas_closet.MainActivity +import com.example.belindas_closet.data.network.dto.auth_dto.DeleteRequest +import com.example.belindas_closet.data.network.dto.auth_dto.DeleteResponse +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.DEFAULT +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +interface DeleteService { + suspend fun delete(deleteRequest: DeleteRequest) : DeleteResponse? + + companion object { + fun create() : DeleteService { + return DeleteServiceImpl( + client = HttpClient(Android) { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } + install (Logging) { + level = LogLevel.ALL + logger = Logger.DEFAULT + } + }, + getToken = suspend { + MainActivity.getPref().getString("token", "") ?: "" + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt new file mode 100644 index 00000000..42f7c46a --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt @@ -0,0 +1,48 @@ +package com.example.belindas_closet.data.network.auth + +import com.example.belindas_closet.data.network.HttpRoutes +import com.example.belindas_closet.data.network.dto.auth_dto.DeleteRequest +import com.example.belindas_closet.data.network.dto.auth_dto.DeleteResponse +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.RedirectResponseException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.request.header +import io.ktor.client.request.put +import io.ktor.client.request.url +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.util.InternalAPI +import kotlinx.serialization.json.Json + +class DeleteServiceImpl ( + private val client: HttpClient, + private val getToken: suspend () -> String +) : DeleteService { + @OptIn(InternalAPI::class) + override suspend fun delete(deleteRequest: DeleteRequest): DeleteResponse? { + return try { + val token = getToken() + val response = client.put { + url("${HttpRoutes.DELETE}/${deleteRequest.id}") + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + header(HttpHeaders.Authorization, "Bearer $token") + body = Json.encodeToString(DeleteRequest.serializer(), deleteRequest) + } + response.body() + } catch (e: RedirectResponseException) { + println("Error: ${e.response.status.description}") + null + } catch (e: ClientRequestException) { + println("Error: ${e.response.status.description}") + null + } catch (e: ServerResponseException) { + println("Error: ${e.response.status.description}") + null + } catch (e: Exception) { + println("Error: ${e.message}") + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteRequest.kt b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteRequest.kt new file mode 100644 index 00000000..34f60c8c --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteRequest.kt @@ -0,0 +1,13 @@ +package com.example.belindas_closet.data.network.dto.auth_dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DeleteRequest( + @SerialName("id") + val id: String, + + @SerialName("role") + val role: Role +) diff --git a/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteResponse.kt b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteResponse.kt new file mode 100644 index 00000000..07862b9f --- /dev/null +++ b/app/src/main/java/com/example/belindas_closet/data/network/dto/auth_dto/DeleteResponse.kt @@ -0,0 +1,10 @@ +package com.example.belindas_closet.data.network.dto.auth_dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DeleteResponse( + @SerialName("isHidden") + val isHidden: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt index 67667174..3c57751a 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt @@ -53,7 +53,9 @@ import com.example.belindas_closet.R import com.example.belindas_closet.Routes import com.example.belindas_closet.data.Datasource import com.example.belindas_closet.data.network.auth.ArchiveService +import com.example.belindas_closet.data.network.auth.DeleteService import com.example.belindas_closet.data.network.dto.auth_dto.ArchiveRequest +import com.example.belindas_closet.data.network.dto.auth_dto.DeleteRequest import com.example.belindas_closet.data.network.dto.auth_dto.Role import com.example.belindas_closet.model.Product import com.example.belindas_closet.model.ProductSizes @@ -192,8 +194,10 @@ fun UpdateIndividualProductCard(product: Product, navController: NavController) editor.putStringSet("hidden", hidden) editor.apply() navController.navigate(Routes.ProductDetail.route) - // TODO: Delete the product from the database // Remove the product from the database + coroutineScope.launch { + delete(product.id, navController, current) + } isDelete = false }, onDismiss = { isDelete = false @@ -366,6 +370,7 @@ fun ConfirmationArchiveDialogIndividual( } } } + @Composable fun ConfirmSaveDialogIndividual( onConfirm: () -> Unit, @@ -478,4 +483,27 @@ suspend fun archive(productId: String, navController: NavController, current: Co println("Error: ${e.message}") Toast.makeText(current, "Archive failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() } +} + +suspend fun delete(productId: String, navController: NavController, current: Context) { + return try { + val deleteRequest = DeleteRequest( + id = productId, + role = Role.ADMIN + ) + val deleteResponse = DeleteService.create().delete(deleteRequest) + if (productId.count() != 24) { + Toast.makeText(current, R.string.delete_invalid_id, Toast.LENGTH_SHORT).show() + } else if (deleteResponse != null) { + MainActivity.getPref().edit().putBoolean("isHidden", deleteResponse.isHidden).apply() + navController.navigate(Routes.ProductDetail.route) + Toast.makeText(current, R.string.delete_successful_toast, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(current, R.string.delete_failed_toast, Toast.LENGTH_SHORT).show() + } + } catch (e: HttpException) { + e.printStackTrace() + println("Error: ${e.message}") + Toast.makeText(current, "Delete failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98e1889d..c450f3c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,6 +48,9 @@ Archival successful! Invalid ID! Archival failed, please try again! + Deletion successful! + Invalid ID! + Deletion failed, please try again! Forgot Password? From 6ad2cd9f0cc73d106570d740afff4559724c50b3 Mon Sep 17 00:00:00 2001 From: Intisar Osman Date: Sat, 25 Nov 2023 05:14:12 -0800 Subject: [PATCH 2/3] Fixed bugs - Fixed bug where deleting an item deletes all items from the same category - Fixed bug where items with invalid id still delete (when they shouldn't - Fixed non-admins being able to delete items --- .../screen/IndividualProductUpdate.kt | 68 +++++++++++-------- .../example/belindas_closet/screen/Login.kt | 22 +++++- .../belindas_closet/screen/ProductDetail.kt | 2 +- app/src/main/res/values/strings.xml | 9 +-- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt index 3c57751a..17c1148b 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/IndividualProductUpdate.kt @@ -188,16 +188,18 @@ fun UpdateIndividualProductCard(product: Product, navController: NavController) } if (isDelete) { ConfirmationDialogIndividual(onConfirm = { - val hidden = MainActivity.getPref().getStringSet("hidden", mutableSetOf(product.productType.name)) - hidden?.add(product.productType.name) - val editor = MainActivity.getPref().edit() - editor.putStringSet("hidden", hidden) - editor.apply() - navController.navigate(Routes.ProductDetail.route) - // Remove the product from the database coroutineScope.launch { - delete(product.id, navController, current) + val isDeleteSuccessful = delete(product.id, navController, current) + if (isDeleteSuccessful) { + val hidden = MainActivity.getPref().getStringSet("hidden", mutableSetOf(product.id)) + hidden?.add(product.id) + val editor = MainActivity.getPref().edit() + editor.putStringSet("hidden", hidden) + editor.apply() + navController.navigate(Routes.ProductDetail.route) + } } + // Remove the product from the database isDelete = false }, onDismiss = { isDelete = false @@ -462,48 +464,58 @@ fun ConfirmCancelDialogIndividual( } } -suspend fun archive(productId: String, navController: NavController, current: Context) { +suspend fun delete(productId: String, navController: NavController, current: Context): Boolean { return try { - val archiveRequest = ArchiveRequest( + val userRole = MainActivity.getPref().getString("userRole", Role.USER.name)?.let { + Role.valueOf(it) } ?: Role.USER + val deleteRequest = DeleteRequest( id = productId, role = Role.ADMIN ) - val archiveResponse = ArchiveService.create().archive(archiveRequest) - if (productId.count() != 24) { - Toast.makeText(current, R.string.archive_invalid_id, Toast.LENGTH_SHORT).show() - } else if (archiveResponse != null) { - MainActivity.getPref().edit().putBoolean("isSold", archiveResponse.isSold).apply() + val deleteResponse = DeleteService.create().delete(deleteRequest) + if (userRole != Role.ADMIN) { + Toast.makeText(current, R.string.unauthorized_toast, Toast.LENGTH_SHORT).show() + false + } else if (productId.count() != 24) { + Toast.makeText(current, R.string.invalid_id, Toast.LENGTH_SHORT).show() + false + } else if (deleteResponse != null) { + MainActivity.getPref().edit().putBoolean("isHidden", deleteResponse.isHidden).apply() navController.navigate(Routes.ProductDetail.route) - Toast.makeText(current, R.string.archive_successful_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(current, R.string.delete_successful_toast, Toast.LENGTH_SHORT).show() + true } else { - Toast.makeText(current, R.string.archive_failed_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(current, R.string.delete_failed_toast, Toast.LENGTH_SHORT).show() + false } } catch (e: HttpException) { e.printStackTrace() println("Error: ${e.message}") - Toast.makeText(current, "Archive failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() + Toast.makeText(current, "Delete failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() + false } } -suspend fun delete(productId: String, navController: NavController, current: Context) { +suspend fun archive(productId: String, navController: NavController, current: Context) { return try { - val deleteRequest = DeleteRequest( + val archiveRequest = ArchiveRequest( id = productId, role = Role.ADMIN ) - val deleteResponse = DeleteService.create().delete(deleteRequest) + val archiveResponse = ArchiveService.create().archive(archiveRequest) if (productId.count() != 24) { - Toast.makeText(current, R.string.delete_invalid_id, Toast.LENGTH_SHORT).show() - } else if (deleteResponse != null) { - MainActivity.getPref().edit().putBoolean("isHidden", deleteResponse.isHidden).apply() + Toast.makeText(current, R.string.invalid_id, Toast.LENGTH_SHORT).show() + } else if (archiveResponse != null) { + MainActivity.getPref().edit().putBoolean("isSold", archiveResponse.isSold).apply() navController.navigate(Routes.ProductDetail.route) - Toast.makeText(current, R.string.delete_successful_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(current, R.string.archive_successful_toast, Toast.LENGTH_SHORT).show() } else { - Toast.makeText(current, R.string.delete_failed_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(current, R.string.archive_failed_toast, Toast.LENGTH_SHORT).show() } } catch (e: HttpException) { e.printStackTrace() println("Error: ${e.message}") - Toast.makeText(current, "Delete failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() + Toast.makeText(current, "Archive failed. Error: ${e.message}", Toast.LENGTH_SHORT).show() } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/example/belindas_closet/screen/Login.kt b/app/src/main/java/com/example/belindas_closet/screen/Login.kt index ed3fbd0d..1bcad58d 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/Login.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/Login.kt @@ -63,8 +63,10 @@ import com.example.belindas_closet.R import com.example.belindas_closet.Routes import com.example.belindas_closet.data.network.auth.LoginService import com.example.belindas_closet.data.network.dto.auth_dto.LoginRequest +import com.example.belindas_closet.data.network.dto.auth_dto.Role import kotlinx.coroutines.launch import org.json.JSONObject +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable @@ -307,6 +309,11 @@ suspend fun loginWithValidCredentials(email: String, password: String, navContro if (loginResponse != null) { val token = loginResponse.token saveToken(token) + + //Extract and store user role + val userRole = getUserRole(token) + MainActivity.getPref().edit().putString("userRole", userRole.name).apply() + MainActivity.getPref().edit().putString("token", loginResponse.token).apply() navController.navigate(Routes.AdminView.route) Toast.makeText( @@ -344,6 +351,19 @@ fun getName(token: String): String? { } } +fun getUserRole(token: String): Role { + return try { + val payload = token.split(".")[1] + val decodedPayload = String(Base64.decode(payload, Base64.DEFAULT)) + val jsonObject = JSONObject(decodedPayload) + val roleString = jsonObject.getString("role") + Role.valueOf(roleString.uppercase(Locale.ROOT)) + } catch (e: Exception) { + e.printStackTrace() + Role.USER + } +} + fun saveToken(token: String) { MainActivity.getPref().edit().putString("token", token).apply() -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt b/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt index 09eeb3e9..9ad0ee35 100644 --- a/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt +++ b/app/src/main/java/com/example/belindas_closet/screen/ProductDetail.kt @@ -153,7 +153,7 @@ fun ProductDetailList(products: List, navController: NavController) { ) { items(products .filter { it.productType.type == MainActivity.getProductType() } - .filter { !hidden!!.contains(it.productType.name) }) { product -> + .filter { !hidden!!.contains(it.id) }) { product -> ProductDetailCard(product = product, navController = navController) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c450f3c0..10273e2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,12 +45,13 @@ Are you sure you want to save this product? Are you sure you want to cancel this edit? Update Product - Archival successful! - Invalid ID! - Archival failed, please try again! Deletion successful! - Invalid ID! + Archival successful! + Unauthorized! + Invalid ID! Deletion failed, please try again! + Archival failed, please try again! + Forgot Password? From 65f642e39c0669756efd99f0f1d9422cc01a4350 Mon Sep 17 00:00:00 2001 From: Intisar Osman Date: Sat, 25 Nov 2023 22:56:44 -0800 Subject: [PATCH 3/3] Fixed wrong request *Something I overlooked* --- .../belindas_closet/data/network/auth/DeleteServiceImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt index 42f7c46a..a9ccdeab 100644 --- a/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt +++ b/app/src/main/java/com/example/belindas_closet/data/network/auth/DeleteServiceImpl.kt @@ -8,8 +8,8 @@ import io.ktor.client.call.body import io.ktor.client.plugins.ClientRequestException import io.ktor.client.plugins.RedirectResponseException import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.request.delete import io.ktor.client.request.header -import io.ktor.client.request.put import io.ktor.client.request.url import io.ktor.http.ContentType import io.ktor.http.HttpHeaders @@ -24,7 +24,7 @@ class DeleteServiceImpl ( override suspend fun delete(deleteRequest: DeleteRequest): DeleteResponse? { return try { val token = getToken() - val response = client.put { + val response = client.delete { url("${HttpRoutes.DELETE}/${deleteRequest.id}") header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) header(HttpHeaders.Authorization, "Bearer $token")