docs: add LDAP avatar integration guide to README
Documents the lldap service account setup, avatar endpoint pattern, CSS photo-over-initials approach, and HTML template used in tinker_tickets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,8 @@ A single-file design system (`base.css` + `base.js`) used across all LotusGuild
|
|||||||
9. [Theming (Dark / Light)](#theming)
|
9. [Theming (Dark / Light)](#theming)
|
||||||
10. [Accessibility](#accessibility)
|
10. [Accessibility](#accessibility)
|
||||||
11. [File Structure](#file-structure)
|
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":"<admin>","password":"<pw>"}' | 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=<admin>,ou=people,dc=example,dc=com" \
|
||||||
|
-w '<admin-pw>' \
|
||||||
|
-s '<service-account-pw>' \
|
||||||
|
"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="<service-account-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
|
||||||
|
<?php
|
||||||
|
$words = array_filter(explode(' ', $displayName));
|
||||||
|
$initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($words, 0, 2))));
|
||||||
|
$colors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||||
|
$color = $colors[abs(crc32($displayName)) % count($colors)];
|
||||||
|
?>
|
||||||
|
<div class="lt-avatar lt-avatar--sm <?= $color ?>" aria-hidden="true">
|
||||||
|
<?php if ($userId > 0): ?>
|
||||||
|
<img src="/api/user_avatar.php?user_id=<?= $userId ?>"
|
||||||
|
alt=""
|
||||||
|
class="lt-avatar-img"
|
||||||
|
onerror="this.style.display='none'">
|
||||||
|
<?php endif ?>
|
||||||
|
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- Always render the initials — they are the fallback, never a broken state
|
||||||
|
- The `onerror` on the `<img>` 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
|
## Changelog
|
||||||
|
|
||||||
### v1.2 (current)
|
### v1.2 (current)
|
||||||
|
|||||||
Reference in New Issue
Block a user