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:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user