mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(avatar): self-hosted avatar storage replacing the Gravatar leak
The previous /me payload carried a gravatarUrl built from
SHA-256(email) and pointed every authenticated page-load at
www.gravatar.com — Automattic could correlate the digest against
its known-email table on each request. This release moves the
avatar in-house: the user uploads through POST /api/user/avatar
(multipart, JPEG/PNG/WebP, 2 MiB / 2048×2048 max), the bytes ride
on the User row alongside a content-type and updatedAt timestamp,
and GET /api/user/avatar/{id} serves them from same-origin with
the cache-busting timestamp on the profile response.
The image-header parsing is hand-rolled in src/lib/avatar.ts so
the standalone image does not gain a native dep just to read width
and height. Validation rejects oversize / wrong content-type at
the multipart parse boundary; DELETE clears the row. The bytes
ride pg_dump alongside the rest of the row so the existing weekly
encrypted backup path already covers them.
The sidebar, the coach panel, the use-auth hook, and the disable-
coach card stop reading gravatarUrl; src/lib/gravatar.ts retires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e93feaf8d1
commit
ca1c52ffcb
20 files changed
+1470
-46
No files matched your search
@@ -1418,6 +1418,109 @@ paths:
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
/api/user/avatar:
|
||||
post:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Upload the calling user's avatar
|
||||
description: "Multipart upload (field `file`). Accepts image/jpeg, image/png, image/webp; rejects anything else at the
|
||||
magic-byte sniff (the multipart Content-Type header is informational only). Hard caps: 2 MiB body, 2048×2048
|
||||
dimensions. Replaces the v1.4.22 Gravatar leak by storing the bytes on the User row + serving them from
|
||||
same-origin."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
description: Binary image payload (JPEG / PNG / WebP). Hard-capped at 2 MiB and 2048×2048.
|
||||
required:
|
||||
- file
|
||||
responses:
|
||||
"201":
|
||||
description: Avatar saved. `avatarUrl` is the same value the /me payload returns; the `?v=` suffix busts client caches
|
||||
on re-upload.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UploadAvatarResponse"
|
||||
"401": *a1
|
||||
"413":
|
||||
description: Upload exceeds the 2 MiB byte limit or the 2048×2048 dimension limit.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"415":
|
||||
description: Image format is not one of JPEG / PNG / WebP (magic-byte sniff).
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
delete:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Clear the calling user's avatar
|
||||
description: Resets the avatar columns to null. Idempotent — a delete on an already-empty row returns 204 with no
|
||||
audit-log row.
|
||||
responses:
|
||||
"204":
|
||||
description: Avatar cleared (or already empty).
|
||||
"401": *a1
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
/api/user/avatar/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Auth
|
||||
summary: Serve a user's avatar bytes
|
||||
description: Owner-scoped. The `{id}` path segment must match the calling user's id — cross-user reads return 403.
|
||||
Response body is the raw image bytes with the persisted `Content-Type`; the /me payload appends
|
||||
`?v={updatedAtMs}` so clients can cache aggressively.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
description: User id (must match the calling user).
|
||||
required: true
|
||||
description: User id (must match the calling user).
|
||||
responses:
|
||||
"200":
|
||||
description: Avatar bytes.
|
||||
content:
|
||||
image/jpeg:
|
||||
schema:
|
||||
type: string
|
||||
description: Raw JPEG bytes.
|
||||
image/png:
|
||||
schema:
|
||||
type: string
|
||||
description: Raw PNG bytes.
|
||||
image/webp:
|
||||
schema:
|
||||
type: string
|
||||
description: Raw WebP bytes.
|
||||
"401": *a1
|
||||
"403":
|
||||
description: Caller is not the avatar's owner.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"404":
|
||||
description: The user has no uploaded avatar.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorEnvelope"
|
||||
"422": *a2
|
||||
"429": *a3
|
||||
/api/insights/chat/messages/{id}/feedback:
|
||||
post:
|
||||
tags:
|
||||
@@ -4160,6 +4263,41 @@ components:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
UploadAvatarResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
avatarUrl:
|
||||
type: string
|
||||
contentType:
|
||||
type: string
|
||||
enum:
|
||||
- image/jpeg
|
||||
- image/png
|
||||
- image/webp
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
|
||||
required:
|
||||
- avatarUrl
|
||||
- contentType
|
||||
- updatedAt
|
||||
additionalProperties: false
|
||||
error:
|
||||
type: "null"
|
||||
meta:
|
||||
type: object
|
||||
properties:
|
||||
requestId:
|
||||
type: string
|
||||
additionalProperties: false
|
||||
required:
|
||||
- data
|
||||
- error
|
||||
additionalProperties: false
|
||||
CoachMessageFeedbackResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
Reference in new issue
Block a user