Added the profile feature

This commit is contained in:
2025-08-05 10:51:31 -06:00
parent f7bd6643cb
commit 2d4214562a
18 changed files with 1377 additions and 0 deletions

View File

@@ -16,6 +16,11 @@ import com.atridad.mealient.datasource.models.GetUserFavoritesResponse
import com.atridad.mealient.datasource.models.GetUserInfoResponse
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
import com.atridad.mealient.datasource.models.UserProfileResponse
import com.atridad.mealient.datasource.models.UpdateUserProfileRequest
import com.atridad.mealient.datasource.models.UpdateUserResponse
import com.atridad.mealient.datasource.models.ChangePasswordRequest
import com.atridad.mealient.datasource.models.UpdateProfileImageRequest
import com.atridad.mealient.datasource.models.VersionResponse
interface MealieDataSource {
@@ -83,4 +88,13 @@ interface MealieDataSource {
suspend fun updateShoppingListName(id: String, name: String)
suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse
// User Profile Management
suspend fun getUserProfile(): UserProfileResponse
suspend fun updateUserProfile(userId: String, request: UpdateUserProfileRequest): UpdateUserResponse
suspend fun changePassword(request: ChangePasswordRequest)
suspend fun updateProfileImage(userId: String, request: UpdateProfileImageRequest)
}

View File

@@ -17,6 +17,11 @@ import com.atridad.mealient.datasource.models.GetUserInfoResponse
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
import com.atridad.mealient.datasource.models.VersionResponse
import com.atridad.mealient.datasource.models.UserProfileResponse
import com.atridad.mealient.datasource.models.UpdateUserProfileRequest
import com.atridad.mealient.datasource.models.UpdateUserResponse
import com.atridad.mealient.datasource.models.ChangePasswordRequest
import com.atridad.mealient.datasource.models.UpdateProfileImageRequest
import kotlinx.serialization.json.JsonElement
internal interface MealieService {
@@ -73,4 +78,13 @@ internal interface MealieService {
suspend fun getShoppingListJson(id: String): JsonElement
suspend fun getUserFavoritesAlternative(userId: String): GetUserFavoritesResponse
// User Profile Management
suspend fun getUserProfile(): UserProfileResponse
suspend fun updateUserProfile(userId: String, request: UpdateUserProfileRequest): UpdateUserResponse
suspend fun changePassword(request: ChangePasswordRequest)
suspend fun updateProfileImage(userId: String, request: UpdateProfileImageRequest)
}

View File

@@ -21,6 +21,11 @@ import com.atridad.mealient.datasource.models.GetUserFavoritesResponse
import com.atridad.mealient.datasource.models.GetUserInfoResponse
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
import com.atridad.mealient.datasource.models.UserProfileResponse
import com.atridad.mealient.datasource.models.UpdateUserProfileRequest
import com.atridad.mealient.datasource.models.UpdateUserResponse
import com.atridad.mealient.datasource.models.ChangePasswordRequest
import com.atridad.mealient.datasource.models.UpdateProfileImageRequest
import com.atridad.mealient.datasource.models.VersionResponse
import io.ktor.client.call.NoTransformationFoundException
import io.ktor.client.call.body
@@ -330,4 +335,39 @@ constructor(
return response
}
// User Profile Management
override suspend fun getUserProfile(): UserProfileResponse {
val response = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getUserProfile() },
logMethod = { "getUserProfile" },
)
return response
}
override suspend fun updateUserProfile(userId: String, request: UpdateUserProfileRequest): UpdateUserResponse {
val response = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateUserProfile(userId, request) },
logMethod = { "updateUserProfile" },
logParameters = { "userId = $userId" }
)
return response
}
override suspend fun changePassword(request: ChangePasswordRequest) {
networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.changePassword(request) },
logMethod = { "changePassword" },
)
}
override suspend fun updateProfileImage(userId: String, request: UpdateProfileImageRequest) {
networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateProfileImage(userId, request) },
logMethod = { "updateProfileImage" },
logParameters = { "userId = $userId" }
)
}
}

View File

@@ -18,18 +18,27 @@ import com.atridad.mealient.datasource.models.GetUserFavoritesResponse
import com.atridad.mealient.datasource.models.GetUserInfoResponse
import com.atridad.mealient.datasource.models.ParseRecipeURLRequest
import com.atridad.mealient.datasource.models.UpdateRecipeRequest
import com.atridad.mealient.datasource.models.UserProfileResponse
import com.atridad.mealient.datasource.models.UpdateUserProfileRequest
import com.atridad.mealient.datasource.models.UpdateUserResponse
import com.atridad.mealient.datasource.models.ChangePasswordRequest
import com.atridad.mealient.datasource.models.UpdateProfileImageRequest
import com.atridad.mealient.datasource.models.VersionResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.client.request.get
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import io.ktor.http.URLBuilder
import io.ktor.http.contentType
import io.ktor.http.parameters
@@ -228,6 +237,47 @@ constructor(
endpoint(baseUrl = baseUrl, path = path, block = block)
}
// User Profile Management
override suspend fun getUserProfile(): UserProfileResponse {
return httpClient.get { endpoint("/api/users/self") }.body()
}
override suspend fun updateUserProfile(userId: String, request: UpdateUserProfileRequest): UpdateUserResponse {
return httpClient.put {
endpoint("/api/users/$userId")
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
override suspend fun changePassword(request: ChangePasswordRequest) {
httpClient.put {
endpoint("/api/users/password")
contentType(ContentType.Application.Json)
setBody(request)
}
}
override suspend fun updateProfileImage(userId: String, request: UpdateProfileImageRequest) {
httpClient.post {
endpoint("/api/users/$userId/image")
setBody(
MultiPartFormDataContent(
formData {
append(
"profile_image",
request.imageBytes,
Headers.build {
append(HttpHeaders.ContentType, request.mimeType)
append(HttpHeaders.ContentDisposition, "filename=\"${request.fileName}\"")
}
)
}
)
)
}
}
private fun HttpRequestBuilder.endpoint(
baseUrl: String,
path: String,
@@ -239,4 +289,6 @@ constructor(
block()
}
}
}

View File

@@ -0,0 +1,124 @@
package com.atridad.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserProfileResponse(
@SerialName("id") val id: String,
@SerialName("username") val username: String?,
@SerialName("fullName") val fullName: String?,
@SerialName("email") val email: String,
@SerialName("authMethod") val authMethod: String? = null,
@SerialName("admin") val admin: Boolean,
@SerialName("group") val group: String? = null,
@SerialName("household") val household: String? = null,
@SerialName("advanced") val advanced: Boolean? = null,
@SerialName("canInvite") val canInvite: Boolean? = null,
@SerialName("canManage") val canManage: Boolean? = null,
@SerialName("canManageHousehold") val canManageHousehold: Boolean? = null,
@SerialName("canOrganize") val canOrganize: Boolean? = null,
@SerialName("groupId") val groupId: String? = null,
@SerialName("groupSlug") val groupSlug: String? = null,
@SerialName("householdId") val householdId: String? = null,
@SerialName("householdSlug") val householdSlug: String? = null,
@SerialName("tokens") val tokens: List<ApiToken>? = null,
@SerialName("cacheKey") val cacheKey: String? = null,
)
@Serializable
data class ApiToken(
@SerialName("name") val name: String,
@SerialName("id") val id: Int,
@SerialName("createdAt") val createdAt: String,
)
@Serializable
data class UpdateUserResponse(
@SerialName("message") val message: String,
@SerialName("error") val error: Boolean,
)
@Serializable
data class UpdateUserProfileRequest(
@SerialName("id") val id: String,
@SerialName("username") val username: String?,
@SerialName("fullName") val fullName: String?,
@SerialName("email") val email: String,
@SerialName("authMethod") val authMethod: String,
@SerialName("admin") val admin: Boolean,
@SerialName("group") val group: String?,
@SerialName("household") val household: String?,
@SerialName("advanced") val advanced: Boolean,
@SerialName("canInvite") val canInvite: Boolean,
@SerialName("canManage") val canManage: Boolean,
@SerialName("canManageHousehold") val canManageHousehold: Boolean,
@SerialName("canOrganize") val canOrganize: Boolean,
@SerialName("groupId") val groupId: String?,
@SerialName("groupSlug") val groupSlug: String?,
@SerialName("householdId") val householdId: String?,
@SerialName("householdSlug") val householdSlug: String?,
@SerialName("tokens") val tokens: List<ApiToken>?,
@SerialName("cacheKey") val cacheKey: String?,
)
// Helper to create an update request from existing profile, preserving all permissions
fun UserProfileResponse.toUpdateRequest(
newFullName: String? = null,
newEmail: String? = null,
newUsername: String? = null
): UpdateUserProfileRequest {
return UpdateUserProfileRequest(
id = this.id,
username = newUsername ?: this.username,
fullName = newFullName ?: this.fullName,
email = newEmail ?: this.email,
authMethod = this.authMethod ?: "Mealie",
admin = this.admin, // Preserve existing admin status
group = this.group,
household = this.household,
advanced = this.advanced ?: true,
canInvite = this.canInvite ?: true,
canManage = this.canManage ?: true,
canManageHousehold = this.canManageHousehold ?: true,
canOrganize = this.canOrganize ?: true,
groupId = this.groupId,
groupSlug = this.groupSlug,
householdId = this.householdId,
householdSlug = this.householdSlug,
tokens = this.tokens,
cacheKey = this.cacheKey,
)
}
@Serializable
data class ChangePasswordRequest(
@SerialName("currentPassword") val currentPassword: String = "",
@SerialName("newPassword") val newPassword: String,
)
data class UpdateProfileImageRequest(
val imageBytes: ByteArray,
val fileName: String,
val mimeType: String = "image/jpeg"
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as UpdateProfileImageRequest
if (!imageBytes.contentEquals(other.imageBytes)) return false
if (fileName != other.fileName) return false
if (mimeType != other.mimeType) return false
return true
}
override fun hashCode(): Int {
var result = imageBytes.contentHashCode()
result = 31 * result + fileName.hashCode()
result = 31 * result + mimeType.hashCode()
return result
}
}