From 6a6977ddff3a3d7253b48bec1960c7c51ed884e2 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Sun, 1 Mar 2026 16:48:56 +0300 Subject: [PATCH] Enhance user profile management and Gravatar integration - Added email field to user profile API and frontend components for better user identification. - Implemented PATCH /map/api/me endpoint to update user email, enhancing user experience. - Introduced useGravatarUrl composable for generating Gravatar URLs based on user email. - Updated profile and layout components to display user avatars using Gravatar, improving visual consistency. - Enhanced development documentation to guide testing of navbar and profile features. --- docs/api.md | 3 +- docs/development.md | 4 + frontend-nuxt/composables/useGravatarUrl.ts | 16 ++++ frontend-nuxt/composables/useMapApi.ts | 9 ++ frontend-nuxt/layouts/default.vue | 65 +++++++++++---- frontend-nuxt/package-lock.json | 37 ++++++++- frontend-nuxt/package.json | 1 + frontend-nuxt/pages/profile.vue | 92 +++++++++++++++++++-- frontend-nuxt/types/api.ts | 1 + internal/app/app.go | 1 + internal/app/handlers/api.go | 9 +- internal/app/handlers/auth.go | 32 +++++++ internal/app/handlers/handlers_test.go | 45 ++++++++++ internal/app/services/auth.go | 24 ++++++ 14 files changed, 311 insertions(+), 28 deletions(-) create mode 100644 frontend-nuxt/composables/useGravatarUrl.ts diff --git a/docs/api.md b/docs/api.md index 2a86615..f0abe6c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -5,7 +5,7 @@ The API is available under the `/map/api/` prefix. Requests requiring authentica ## Authentication - **POST /map/api/login** — sign in. Body: `{"user":"...","pass":"..."}`. On success returns JSON with user data and sets a session cookie. On first run, bootstrap is available: logging in as `admin` with the password from `HNHMAP_BOOTSTRAP_PASSWORD` creates the first admin user. For users created via OAuth (no password), returns 401 with `{"error":"Use OAuth to sign in"}`. -- **GET /map/api/me** — current user (by session). Response: `username`, `auths`, and optionally `tokens`, `prefix`. +- **GET /map/api/me** — current user (by session). Response: `username`, `auths`, and optionally `tokens`, `prefix`, `email` (string, optional — for Gravatar and display). - **POST /map/api/logout** — sign out (invalidates the session). - **GET /map/api/setup** — check if initial setup is needed. Response: `{"setupRequired": true|false}`. @@ -17,6 +17,7 @@ The API is available under the `/map/api/` prefix. Requests requiring authentica ## User account +- **PATCH /map/api/me** — update current user. Body: `{"email": "..."}`. Used to set or change the user's email (for Gravatar and profile display). Requires a valid session. - **POST /map/api/me/tokens** — generate a new upload token (requires `upload` permission). Response: `{"tokens": ["...", ...]}`. - **POST /map/api/me/password** — change password. Body: `{"pass":"..."}`. diff --git a/docs/development.md b/docs/development.md index c6b93bd..23e34e0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,6 +49,10 @@ The dev Compose setup starts two services: Use [http://localhost:3000/](http://localhost:3000/) as the primary URL for UI development. Port `3080` is for API and backend endpoints; the root `/` may return `404` in dev mode — this is expected. +**Testing navbar and profile:** With no users in the database, log in as `admin` using the bootstrap password (e.g. `HNHMAP_BOOTSTRAP_PASSWORD=admin` in docker-compose.dev) to create the first admin user. You can then use that account to verify the navbar avatar and profile page. For Gravatar, use OAuth (e.g. Google) or set email later on the profile page once that feature is available. + +**Gravatar (avatar by email):** Gravatar URLs are built on the frontend using the `md5` package (client-side MD5 of the user's email). No backend endpoint is used; the frontend composable `useGravatarUrl` (see Phase 5+ of the navbar/avatar plan) will use this dependency. + ### Building the image and production Compose ```bash diff --git a/frontend-nuxt/composables/useGravatarUrl.ts b/frontend-nuxt/composables/useGravatarUrl.ts new file mode 100644 index 0000000..d908d7a --- /dev/null +++ b/frontend-nuxt/composables/useGravatarUrl.ts @@ -0,0 +1,16 @@ +import md5 from 'md5' + +/** + * Returns Gravatar avatar URL for the given email, or empty string if no email. + * Gravatar expects: trim, lowercase, then MD5 hex. Use empty string to show a placeholder (e.g. initial letter) in the UI. + * + * @param email - User email (optional) + * @param size - Avatar size in pixels (default 64; navbar 32, drawer 40, profile 64–80) + */ +export function useGravatarUrl(email: string | undefined, size?: number): string { + const normalized = email?.trim().toLowerCase() + if (!normalized) return '' + const hash = md5(normalized) + const s = size ?? 64 + return `https://www.gravatar.com/avatar/${hash}?s=${s}&d=identicon` +} diff --git a/frontend-nuxt/composables/useMapApi.ts b/frontend-nuxt/composables/useMapApi.ts index 5507a36..f911656 100644 --- a/frontend-nuxt/composables/useMapApi.ts +++ b/frontend-nuxt/composables/useMapApi.ts @@ -99,6 +99,14 @@ export function useMapApi() { return request('me') } + async function meUpdate(body: { email?: string }) { + await request('me', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + } + /** Public: whether first-time setup (no users) is required. */ async function setupRequired(): Promise<{ setupRequired: boolean }> { const res = await fetch(`${apiBase}/setup`, { credentials: 'include' }) @@ -225,6 +233,7 @@ export function useMapApi() { login, logout, me, + meUpdate, oauthLoginUrl, oauthProviders, setupRequired, diff --git a/frontend-nuxt/layouts/default.vue b/frontend-nuxt/layouts/default.vue index fcc9698..508c992 100644 --- a/frontend-nuxt/layouts/default.vue +++ b/frontend-nuxt/layouts/default.vue @@ -19,30 +19,39 @@