diff --git a/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/MeApi.kt b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/MeApi.kt new file mode 100644 index 00000000..6781ef2b --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/api/endpoints/MeApi.kt @@ -0,0 +1,36 @@ +package com.fabledsword.minstrel.api.endpoints + +import com.fabledsword.minstrel.models.wire.ChangePasswordRequest +import com.fabledsword.minstrel.models.wire.MyProfileWire +import com.fabledsword.minstrel.models.wire.UpdateProfileRequest +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PUT + +/** + * Retrofit interface for `/api/me/*` — caller-scoped account endpoints. + * Mirrors the relevant slice of `flutter_client/lib/api/endpoints/settings.dart`. + * + * History + timezone + system-playlists-status live under /api/me too + * but are handled by their respective feature repositories; this + * interface only carries the profile + password slice. + */ +interface MeApi { + @GET("api/me") + suspend fun getProfile(): MyProfileWire + + /** + * Pass only the fields you want to change; server merges. Returns + * the canonical post-update row so the caller can refresh local + * state without an extra GET. + */ + @PUT("api/me/profile") + suspend fun updateProfile(@Body body: UpdateProfileRequest): MyProfileWire + + /** + * Change the caller's password. Server validates `current_password` + * before accepting `new_password`. No body on success (204). + */ + @PUT("api/me/password") + suspend fun changePassword(@Body body: ChangePasswordRequest) +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/MyProfile.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/MyProfile.kt new file mode 100644 index 00000000..44695010 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/MyProfile.kt @@ -0,0 +1,15 @@ +package com.fabledsword.minstrel.models + +/** + * Domain shape for the caller's account profile. Mirrors Flutter's + * `MyProfile`. `displayName` and `email` are nullable — server stores + * null when the user hasn't set them yet (registration only requires + * a username). + */ +data class MyProfile( + val id: String, + val username: String, + val displayName: String?, + val email: String?, + val isAdmin: Boolean, +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/wire/MyProfileWire.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/MyProfileWire.kt new file mode 100644 index 00000000..3cfab9c9 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/wire/MyProfileWire.kt @@ -0,0 +1,41 @@ +package com.fabledsword.minstrel.models.wire + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Wire shape for `GET /api/me` and the return value of + * `PUT /api/me/profile`. Mirrors + * `flutter_client/lib/models/my_profile.dart`: + * - `display_name` and `email` are nullable; server returns null + * when the user hasn't set them yet (registration only requires + * a username). + */ +@Serializable +data class MyProfileWire( + val id: String = "", + val username: String = "", + @SerialName("display_name") val displayName: String? = null, + val email: String? = null, + @SerialName("is_admin") val isAdmin: Boolean = false, +) + +/** + * Body for `PUT /api/me/profile`. Pass only the fields you want to + * change; the server merges — empty strings clear, nulls leave + * existing values alone. + */ +@Serializable +data class UpdateProfileRequest( + @SerialName("display_name") val displayName: String? = null, + val email: String? = null, +) + +/** + * Body for `PUT /api/me/password`. Both fields required. + */ +@Serializable +data class ChangePasswordRequest( + @SerialName("current_password") val currentPassword: String, + @SerialName("new_password") val newPassword: String, +) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/settings/data/MeRepository.kt b/android/app/src/main/java/com/fabledsword/minstrel/settings/data/MeRepository.kt new file mode 100644 index 00000000..e81a5905 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/settings/data/MeRepository.kt @@ -0,0 +1,44 @@ +package com.fabledsword.minstrel.settings.data + +import com.fabledsword.minstrel.api.endpoints.MeApi +import com.fabledsword.minstrel.models.MyProfile +import com.fabledsword.minstrel.models.wire.ChangePasswordRequest +import com.fabledsword.minstrel.models.wire.MyProfileWire +import com.fabledsword.minstrel.models.wire.UpdateProfileRequest +import retrofit2.Retrofit +import retrofit2.create +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Thin facade over `/api/me/*` for the Settings Profile + Password + * cards. No Room caching — the Settings card fetches on screen + * mount and writes go straight to the server. Network errors + * surface to the caller for UI snackbar; no offline-queue path + * because changing your own password offline doesn't make sense. + */ +@Singleton +class MeRepository @Inject constructor(retrofit: Retrofit) { + private val api: MeApi = retrofit.create() + + suspend fun getProfile(): MyProfile = api.getProfile().toDomain() + + suspend fun updateProfile(displayName: String?, email: String?): MyProfile = + api.updateProfile( + UpdateProfileRequest(displayName = displayName, email = email), + ).toDomain() + + suspend fun changePassword(current: String, next: String) { + api.changePassword( + ChangePasswordRequest(currentPassword = current, newPassword = next), + ) + } +} + +private fun MyProfileWire.toDomain(): MyProfile = MyProfile( + id = id, + username = username, + displayName = displayName, + email = email, + isAdmin = isAdmin, +)