feat(oauth): add client_credentials grant for machine clients and fix PlaceAvatar stale image retry

- Add OAuth 2.0 client_credentials flow so AI agents and scripts can obtain tokens directly via client_id + client_secret without any browser interaction
- New DB column allows_client_credentials on oauth_clients; machine clients skip redirect URI requirement and are forced confidential
- New issueClientCredentialsToken() issues access-only tokens (no refresh token, RFC 6749 §4.4)
- UI: "Machine client" checkbox in create-client modal, hides redirect URI field, shows indigo badge on existing machine clients
- Advertise client_credentials in OAuth discovery document
- 8 new integration tests (OAUTH-CC-001–008)
- i18n: 4 new keys across all 15 languages
- Fix PlaceAvatar: re-fetch photo via API on image_url load failure before falling back to initials
- Update MCP wiki docs with new Option B machine client setup guide
This commit is contained in:
jubnl
2026-05-22 14:42:20 +02:00
parent bfe6664ac4
commit c828fca059
25 changed files with 417 additions and 56 deletions
+13 -1
View File
@@ -18,6 +18,7 @@ interface PlaceAvatarProps {
export default React.memo(function PlaceAvatar({ place, size = 32, category }: PlaceAvatarProps) {
const [photoSrc, setPhotoSrc] = useState<string | null>(place.image_url || null)
const [visible, setVisible] = useState(false)
const imageUrlFailed = useRef(false)
const ref = useRef<HTMLDivElement>(null)
const placesPhotosEnabled = useAuthStore(s => s.placesPhotosEnabled)
@@ -86,7 +87,18 @@ export default React.memo(function PlaceAvatar({ place, size = 32, category }: P
alt={place.name}
decoding="async"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={() => setPhotoSrc(null)}
onError={() => {
if (!imageUrlFailed.current && photoSrc === place.image_url && (place.google_place_id || place.osm_id)) {
imageUrlFailed.current = true
const photoId = place.google_place_id || place.osm_id!
const cacheKey = `refetch:${photoId}`
fetchPhoto(cacheKey, photoId, place.lat ?? undefined, place.lng ?? undefined, place.name,
entry => { setPhotoSrc(entry.thumbDataUrl || entry.photoUrl) }
)
} else {
setPhotoSrc(null)
}
}}
/>
</div>
)