mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
feat(oidc): use the picture claim as avatar when none is uploaded
When a user signs in via OIDC and hasn't uploaded a custom avatar, their `picture` claim is now used as their avatar. The users.avatar column holds either an uploaded file name or an absolute https URL from the claim, and a single resolver on each side (server avatarUrl, client avatarSrc) renders both. An uploaded avatar always wins and is never overwritten; the picture refreshes on each login otherwise. Only https URLs are stored, matching the image CSP. All the scattered /uploads/avatars/ builders now go through the resolvers, which also fixes collection member avatars that were rendering a bare file name. Discussion #1399
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkBreaks from 'remark-breaks'
|
||||
@@ -28,7 +29,7 @@ interface NoteCardProps {
|
||||
export function NoteCard({ note, currentUser, canEdit, onUpdate, onDelete, onEdit, onView, onPreviewFile, getCategoryColor, tripId, t }: NoteCardProps) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
const author = note.author || note.user || { username: note.username, avatar: note.avatar_url || (note.avatar ? `/uploads/avatars/${note.avatar}` : null) }
|
||||
const author = note.author || note.user || { username: note.username, avatar: note.avatar_url || avatarSrc(note.avatar) }
|
||||
const color = getCategoryColor ? getCategoryColor(note.category) : (note.color || '#6366f1')
|
||||
|
||||
const handleTogglePin = useCallback(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -179,7 +180,7 @@ export default function WhatsNextWidget({ tripMembers = [] }: WhatsNextWidgetPro
|
||||
overflow: 'hidden', flexShrink: 0,
|
||||
}}>
|
||||
{p.avatar
|
||||
? <img src={`/uploads/avatars/${p.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
? <img src={avatarSrc(p.avatar)!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
: p.username?.[0]?.toUpperCase()
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { Share2, Users, Link2, Pencil } from 'lucide-react'
|
||||
import type { CollectionMember, CollectionLink } from '@trek/shared'
|
||||
import type { TranslationFn } from '../../types'
|
||||
@@ -67,7 +68,7 @@ export default function CollectionHero({
|
||||
<span className="members">
|
||||
{shown.map(m => (
|
||||
m.avatar
|
||||
? <img key={m.user_id} className="col-av" src={m.avatar} alt={m.username} />
|
||||
? <img key={m.user_id} className="col-av" src={avatarSrc(m.avatar)!} alt={m.username} />
|
||||
: <span key={m.user_id} className="col-av" style={{ background: AV_COLORS[m.user_id % AV_COLORS.length] }}>{initials(m.username)}</span>
|
||||
))}
|
||||
{extra > 0 && <span className="col-av" style={{ background: 'rgba(255,255,255,.28)' }}>+{extra}</span>}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { UserPlus, UserMinus, Loader2, Clock, Crown, LogOut } from 'lucide-react'
|
||||
import type { CollectionMember, CollectionRole } from '@trek/shared'
|
||||
import Modal from '../shared/Modal'
|
||||
@@ -29,7 +30,7 @@ function MemberAvatar({ member }: { member: CollectionMember }): React.ReactElem
|
||||
return (
|
||||
<span className="w-8 h-8 rounded-full shrink-0 overflow-hidden flex items-center justify-center bg-surface-secondary text-content-secondary text-[12px] font-semibold">
|
||||
{member.avatar ? (
|
||||
<img src={`/uploads/avatars/${member.avatar}`} alt="" className="w-full h-full object-cover" />
|
||||
<img src={avatarSrc(member.avatar)!} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
initial
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ interface DragDataPayload { placeId?: string; assignmentId?: string; noteId?: st
|
||||
declare global { interface Window { __dragData: DragDataPayload | null } }
|
||||
|
||||
import React, { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { ChevronDown, ChevronRight, ChevronUp, Navigation, RotateCcw, ExternalLink, Clock, Pencil, GripVertical, Ticket, Plus, FileText, Trash2, Car, Lock, Hotel, Footprints, Route as RouteIcon, Bookmark } from 'lucide-react'
|
||||
import { assignmentsApi, reservationsApi } from '../../api/client'
|
||||
import { calculateRoute, calculateRouteWithLegs, optimizeRoute, generateGoogleMapsUrl } from '../Map/RouteCalculator'
|
||||
@@ -1867,7 +1868,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP
|
||||
marginLeft: pi > 0 ? -4 : 0, flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{p.avatar ? <img src={`/uploads/avatars/${p.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : p.username?.[0]?.toUpperCase()}
|
||||
{p.avatar ? <img src={avatarSrc(p.avatar)!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : p.username?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
{assignment.participants.length > 5 && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import { openFile } from '../../utils/fileDownload'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
@@ -542,7 +543,7 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(7px * var(--fs-scale-caption, 1))', fontWeight: 700,
|
||||
overflow: 'hidden', flexShrink: 0,
|
||||
}}>
|
||||
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
|
||||
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || avatarSrc(member.avatar)!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<span style={{ textDecoration: isHovered && canRemove ? 'line-through' : 'none' }}>{member.username}</span>
|
||||
</div>
|
||||
@@ -582,7 +583,7 @@ function ParticipantsBox({ tripMembers, participantIds, allJoined, onSetParticip
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(8px * var(--fs-scale-caption, 1))', fontWeight: 700,
|
||||
overflow: 'hidden', flexShrink: 0,
|
||||
}}>
|
||||
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || `/uploads/avatars/${member.avatar}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
|
||||
{(member.avatar_url || member.avatar) ? <img src={member.avatar_url || avatarSrc(member.avatar)!} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> : member.username?.[0]?.toUpperCase()}
|
||||
</div>
|
||||
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{member.username}</span>
|
||||
{member.is_guest && <GuestBadge size="xs" />}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useMemo, useEffect, useRef } from 'react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useTripStore } from '../../store/tripStore'
|
||||
import { useCanDo } from '../../store/permissionsStore'
|
||||
@@ -481,7 +482,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: {
|
||||
value: String(m.id),
|
||||
label: m.is_guest ? `${m.username} · ${t('members.guest')}` : m.username,
|
||||
icon: m.avatar ? (
|
||||
<img src={`/uploads/avatars/${m.avatar}`} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
|
||||
<img src={avatarSrc(m.avatar)!} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
|
||||
) : (
|
||||
<span style={{ width: 18, height: 18, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 600 }}>
|
||||
{m.username.charAt(0).toUpperCase()}
|
||||
@@ -671,7 +672,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
|
||||
...members.map(m => ({
|
||||
value: String(m.id), label: m.username,
|
||||
icon: m.avatar ? (
|
||||
<img src={`/uploads/avatars/${m.avatar}`} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
|
||||
<img src={avatarSrc(m.avatar)!} style={{ width: 18, height: 18, borderRadius: '50%', objectFit: 'cover' as const }} alt="" />
|
||||
) : (
|
||||
<span style={{ width: 18, height: 18, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 600 }}>
|
||||
{m.username.charAt(0).toUpperCase()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CheckSquare, Square, ChevronRight, Flag, Calendar, GripVertical, UserRound } from 'lucide-react'
|
||||
import { avatarSrc } from '../../utils/avatarSrc'
|
||||
import type { TodoItem } from '../../types'
|
||||
import { katColor, PRIO_CONFIG, type Member } from './todoListModel'
|
||||
|
||||
@@ -125,7 +126,7 @@ export default function TodoRow({ item, members, categories, today, isSelected,
|
||||
border: '1px solid var(--border-faint)',
|
||||
}}>
|
||||
{assignedUser.avatar ? (
|
||||
<img src={`/uploads/avatars/${assignedUser.avatar}`} style={{ width: 13, height: 13, borderRadius: '50%', objectFit: 'cover' }} alt="" />
|
||||
<img src={avatarSrc(assignedUser.avatar)!} style={{ width: 13, height: 13, borderRadius: '50%', objectFit: 'cover' }} alt="" />
|
||||
) : (
|
||||
<span style={{ width: 13, height: 13, borderRadius: '50%', background: 'var(--border-primary)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 'calc(7px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontWeight: 700 }}>
|
||||
{assignedUser.username.charAt(0).toUpperCase()}
|
||||
|
||||
Reference in New Issue
Block a user