feat(android): MeApi + MyProfile + MeRepository foundation

Adds the /api/me/* slice mirroring flutter_client's SettingsApi:

* GET /api/me → MyProfile (id / username / displayName / email / isAdmin)
* PUT /api/me/profile → merges displayName + email
* PUT /api/me/password → current + new password

No caching — the Settings cards fetch on mount and writes go
straight to the server. No offline-queue fallback (changing your
own password offline is meaningless).

Profile + Password UI cards land in the next commit; this is just
the data layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:13:41 -04:00
parent f7c3bd2dcf
commit f18b7d4658
4 changed files with 136 additions and 0 deletions
@@ -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)
}
@@ -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,
)
@@ -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,
)
@@ -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,
)