feat(auth): add /api/me endpoint, avatar upload, and profile picture support
Build and Deploy / build-and-push (push) Successful in 5m7s
Build and Deploy / build-and-push (push) Successful in 5m7s
- Add migration 005: nullable avatar_url column on users table - Add GET /api/me: returns id, username, avatar_url from DB (fixes UUID-on-profile bug) - Add PUT /api/me/avatar: accepts raw image bytes (≤1 MB, jpeg/png/webp/gif), writes to avatars/ dir, updates avatar_url in DB - Serve /avatars via ServeDir so uploaded images are publicly accessible - Update account.html: fetch username from /api/me instead of parsing JWT; add circular avatar display with initials fallback and click-to-upload - Add SolitaireServerClient::fetch_me() for desktop/Android profile display - Add avatar_url field to SyncBackend::SolitaireServer settings (serde default None) - Update sqlx offline query cache for new avatar_url queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,9 @@ pub enum SyncBackend {
|
||||
url: String,
|
||||
/// The player's username on that server.
|
||||
username: String,
|
||||
/// Absolute URL of the user's avatar image, or `None` if not set.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
avatar_url: Option<String>,
|
||||
// JWT tokens are stored in the OS keychain — not here.
|
||||
},
|
||||
|
||||
|
||||
@@ -500,6 +500,53 @@ impl SolitaireServerClient {
|
||||
})?;
|
||||
Ok(format!("{}/replays/{}", self.base_url, id))
|
||||
}
|
||||
|
||||
/// Fetch the authenticated user's profile (`GET /api/me`).
|
||||
///
|
||||
/// Returns `(username, avatar_url)`. `avatar_url` is `None` when the user
|
||||
/// has not set an avatar. Returns an error on network failure or if the
|
||||
/// token is expired and refresh also fails.
|
||||
pub async fn fetch_me(&self) -> Result<(String, Option<String>), SyncError> {
|
||||
let token = self.access_token()?;
|
||||
let url = format!("{}/api/me", self.base_url);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.bearer_auth(&token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||
|
||||
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
self.refresh_token().await?;
|
||||
let new_token = self.access_token()?;
|
||||
let resp = self
|
||||
.client
|
||||
.get(&url)
|
||||
.bearer_auth(new_token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| SyncError::Network(e.to_string()))?;
|
||||
return Self::extract_me_body(resp).await;
|
||||
}
|
||||
|
||||
Self::extract_me_body(resp).await
|
||||
}
|
||||
|
||||
async fn extract_me_body(resp: reqwest::Response) -> Result<(String, Option<String>), SyncError> {
|
||||
let status = resp.status();
|
||||
if !status.is_success() {
|
||||
return Err(SyncError::Network(format!("GET /api/me returned {status}")));
|
||||
}
|
||||
let body: serde_json::Value = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| SyncError::Serialization(e.to_string()))?;
|
||||
let username = body["username"].as_str().unwrap_or("").to_string();
|
||||
let avatar_url = body["avatar_url"].as_str().map(str::to_string);
|
||||
Ok((username, avatar_url))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -575,7 +622,7 @@ async fn extract_push_body(resp: reqwest::Response) -> Result<SyncResponse, Sync
|
||||
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
||||
match backend {
|
||||
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
||||
SyncBackend::SolitaireServer { url, username } => {
|
||||
SyncBackend::SolitaireServer { url, username, .. } => {
|
||||
Box::new(SolitaireServerClient::new(url.clone(), username.clone()))
|
||||
}
|
||||
}
|
||||
@@ -628,6 +675,7 @@ mod tests {
|
||||
let provider = provider_for_backend(&SyncBackend::SolitaireServer {
|
||||
url: "https://example.com".to_string(),
|
||||
username: "bob".to_string(),
|
||||
avatar_url: None,
|
||||
});
|
||||
assert_eq!(provider.backend_name(), "solitaire_server");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user