fix(backend): cache-bust served avatars with an uploaded_at version
Avatar objects live at a deterministic key (avatars/<id>.png) served with a 300s cache, so after a re-upload the <img> kept pointing at the same URL and the browser showed the stale crop until reload. Append ?v=<uploaded_at epoch> to every avatar_url (me, public profile, and showcase/grpc batch) so the URL changes whenever the avatar changes.
This commit is contained in:
parent
eb854c32da
commit
8a758754dd
3 changed files with 26 additions and 16 deletions
|
|
@ -32,9 +32,9 @@ pub async fn me(
|
||||||
let account = repo::find_by_id(&state.pool, identity.account_id)
|
let account = repo::find_by_id(&state.pool, identity.account_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(AppError::Unauthorized)?;
|
.ok_or(AppError::Unauthorized)?;
|
||||||
let avatar_url = repo::avatar_key(&state.pool, account.id)
|
let avatar_url = repo::avatar_ref(&state.pool, account.id)
|
||||||
.await?
|
.await?
|
||||||
.map(|key| format!("{}/{key}", state.avatar_base_url));
|
.map(|(key, v)| format!("{}/{key}?v={v}", state.avatar_base_url));
|
||||||
Ok(Json(MeResponse {
|
Ok(Json(MeResponse {
|
||||||
id: account.id,
|
id: account.id,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
|
|
@ -69,9 +69,9 @@ pub async fn public_profile(
|
||||||
let account = repo::find_by_id(&state.pool, id)
|
let account = repo::find_by_id(&state.pool, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(not_found)?;
|
.ok_or_else(not_found)?;
|
||||||
let avatar_url = repo::avatar_key(&state.pool, account.id)
|
let avatar_url = repo::avatar_ref(&state.pool, account.id)
|
||||||
.await?
|
.await?
|
||||||
.map(|key| format!("{}/{key}", state.avatar_base_url));
|
.map(|(key, v)| format!("{}/{key}?v={v}", state.avatar_base_url));
|
||||||
let rank = repo::account_rank(&state.pool, account.created_at).await?;
|
let rank = repo::account_rank(&state.pool, account.created_at).await?;
|
||||||
let badges = if rank < EARLY_ADOPTER_LIMIT {
|
let badges = if rank < EARLY_ADOPTER_LIMIT {
|
||||||
vec!["early".to_owned()]
|
vec!["early".to_owned()]
|
||||||
|
|
|
||||||
|
|
@ -58,21 +58,30 @@ pub async fn set_avatar(pool: &PgPool, account_id: Uuid, s3_key: &str) -> Result
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn avatar_key(pool: &PgPool, account_id: Uuid) -> Result<Option<String>, sqlx::Error> {
|
/// Avatar reference for one account: its storage key plus an `uploaded_at`
|
||||||
sqlx::query_scalar("SELECT s3_key FROM avatars WHERE account_id = $1")
|
/// version (epoch seconds) used to cache-bust the served image URL.
|
||||||
.bind(account_id)
|
pub async fn avatar_ref(
|
||||||
.fetch_optional(pool)
|
pool: &PgPool,
|
||||||
.await
|
account_id: Uuid,
|
||||||
|
) -> Result<Option<(String, i64)>, sqlx::Error> {
|
||||||
|
let row: Option<(String, i64)> = sqlx::query_as(
|
||||||
|
"SELECT s3_key, EXTRACT(EPOCH FROM uploaded_at)::bigint FROM avatars WHERE account_id = $1",
|
||||||
|
)
|
||||||
|
.bind(account_id)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nick + avatar key for the given accounts (showcase authors etc.).
|
/// Nick + avatar (key, version) for the given accounts (showcase authors etc.).
|
||||||
/// Missing ids are simply absent from the result.
|
/// Missing ids are simply absent from the result.
|
||||||
pub async fn public_profiles(
|
pub async fn public_profiles(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
ids: &[Uuid],
|
ids: &[Uuid],
|
||||||
) -> Result<Vec<(Uuid, String, Option<String>)>, sqlx::Error> {
|
) -> Result<Vec<(Uuid, String, Option<String>, Option<i64>)>, sqlx::Error> {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT a.id, a.display_nick, av.s3_key FROM accounts a
|
"SELECT a.id, a.display_nick, av.s3_key, EXTRACT(EPOCH FROM av.uploaded_at)::bigint
|
||||||
|
FROM accounts a
|
||||||
LEFT JOIN avatars av ON av.account_id = a.id
|
LEFT JOIN avatars av ON av.account_id = a.id
|
||||||
WHERE a.id = ANY($1)",
|
WHERE a.id = ANY($1)",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -74,12 +74,13 @@ impl AccountsInternal for AccountsGrpc {
|
||||||
})?;
|
})?;
|
||||||
let profiles = rows
|
let profiles = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, nick, key)| PublicProfile {
|
.map(|(id, nick, key, version)| PublicProfile {
|
||||||
account_id: id.to_string(),
|
account_id: id.to_string(),
|
||||||
display_nick: nick,
|
display_nick: nick,
|
||||||
avatar_url: key
|
avatar_url: match (key, version) {
|
||||||
.map(|k| format!("{}/{k}", self.avatar_base_url))
|
(Some(k), Some(v)) => format!("{}/{k}?v={v}", self.avatar_base_url),
|
||||||
.unwrap_or_default(),
|
_ => String::new(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(Response::new(GetPublicProfilesReply { profiles }))
|
Ok(Response::new(GetPublicProfilesReply { profiles }))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue