diff --git a/README.md b/README.md index 6837ffc..08bd086 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ A single-file design system (`base.css` + `base.js`) used across all LotusGuild 9. [Theming (Dark / Light)](#theming) 10. [Accessibility](#accessibility) 11. [File Structure](#file-structure) -12. [Changelog](#changelog) +12. [LDAP Avatar Integration](#ldap-avatar-integration) +13. [Changelog](#changelog) --- @@ -908,6 +909,116 @@ web_template/ --- +## LDAP Avatar Integration + +`lt-avatar` components support real profile photos pulled from **lldap** (the LotusGuild LDAP server). Photos overlay the initials fallback — if a user has no photo the initials show instead; no broken images. + +### Infrastructure (one-time, per app) + +**1. Create a service account in lldap** + +Log into lldap at `http://10.10.10.39:17170`, or use the GraphQL API: + +```bash +TOKEN=$(curl -s -X POST http://10.10.10.39:17170/auth/simple/login \ + -H 'Content-Type: application/json' \ + -d '{"username":"","password":""}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + +# Create service account +curl -s -X POST http://10.10.10.39:17170/api/graphql \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"query":"mutation { createUser(user: { id: \"my-app\", email: \"my-app@lotusguild.org\", displayName: \"My App Service\" }) { id } }"}' + +# Add to lldap_strict_readonly (id=3) for directory read access +curl -s -X POST http://10.10.10.39:17170/api/graphql \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"query":"mutation { addUserToGroup(userId: \"my-app\", groupId: 3) { ok } }"}' +``` + +Then set the password via `ldappasswd`: +```bash +ldappasswd -H ldap://10.10.10.39:3890 \ + -D "uid=,ou=people,dc=example,dc=com" \ + -w '' \ + -s '' \ + "uid=my-app,ou=people,dc=example,dc=com" +``` + +**2. Install php-ldap and create the avatar cache directory** +```bash +apt-get install -y php8.2-ldap +mkdir -p /var/www/html/myapp/uploads/avatars +chown -R www-data:www-data /var/www/html/myapp/uploads/avatars +``` + +**3. Add LDAP config to `.env`** +```ini +LDAP_ENABLED=true +LDAP_HOST=10.10.10.39 +LDAP_PORT=3890 +LDAP_BIND_DN="uid=my-app,ou=people,dc=example,dc=com" +LDAP_BIND_PW="" +LDAP_BASE_DN="dc=example,dc=com" +LDAP_USER_BASE="ou=people,dc=example,dc=com" +AVATAR_CACHE_TTL=3600 +``` + +> **Note:** lldap is currently configured with `dc=example,dc=com` as the base DN across all services (Authelia, etc.). Do not change this per-app — it requires a coordinated infrastructure migration. + +### Avatar Endpoint (`/api/user_avatar.php`) + +Copy the reference implementation from `tinker_tickets/api/user_avatar.php`. It: + +1. Requires a valid session (returns 401 otherwise) +2. Accepts `?user_id=N` and looks up the user's `username` from the app's DB +3. Binds to lldap and searches `ou=people,dc=example,dc=com` with filter `(uid={username})` +4. Fetches the `avatar` attribute (raw binary JPEG, returned as-is by `ldap_get_entries()`) +5. Validates JPEG magic bytes (`\xFF\xD8\xFF`) and writes to `uploads/avatars/user_{id}.jpg` +6. Writes a `.none` sentinel file for users with no avatar so lldap is not queried again until TTL expires +7. Serves the cached file with `Content-Type: image/jpeg` + +### CSS — Photo-over-Initials Pattern + +`base.css` (Section 62 — Avatar) provides `.lt-avatar-img` and `.lt-avatar-initials`: + +```css +.lt-avatar { position: relative; } +.lt-avatar-initials { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } +.lt-avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; z-index: 1; } +``` + +The photo sits above the initials. The `onerror` handler hides the image if the endpoint returns 404 (no avatar), letting the initials show through. + +### HTML Pattern + +```php + $w[0], array_slice($words, 0, 2)))); +$colors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', '']; +$color = $colors[abs(crc32($displayName)) % count($colors)]; +?> + +``` + +Key points: +- Always render the initials — they are the fallback, never a broken state +- The `onerror` on the `` hides it when the endpoint returns 404 (no photo set) +- Color is deterministically derived from the display name so it's consistent across page loads +- `aria-hidden="true"` because the avatar is purely decorative; the user's name appears in adjacent text + +--- + ## Changelog ### v1.2 (current)