diff --git a/web/src/features/settings/ProfileSettingsPage.tsx b/web/src/features/settings/ProfileSettingsPage.tsx index 0958b144..3852fc7b 100644 --- a/web/src/features/settings/ProfileSettingsPage.tsx +++ b/web/src/features/settings/ProfileSettingsPage.tsx @@ -85,9 +85,8 @@ export function ProfileSettingsPage() { const avatarMutation = useMutation({ mutationFn: async (file: File) => { const uploaded = await uploadFile(file); - const fileId = uploaded.url.split('/').pop() || ''; if (!query.data) throw new Error('尚未加载'); - return updateMyProfile({ avatar_file_id: fileId, version: query.data.version }); + return updateMyProfile({ avatar_file_id: uploaded.file_id, version: query.data.version }); }, onSuccess: (data) => { queryClient.setQueryData(['my-profile'], (prev: typeof query.data) => (prev ? { ...prev, ...data } : data)); diff --git a/web/src/lib/api/upload.ts b/web/src/lib/api/upload.ts index 73a4d1da..4790fdad 100644 --- a/web/src/lib/api/upload.ts +++ b/web/src/lib/api/upload.ts @@ -72,6 +72,16 @@ export function fileAuthHeaders(): Record { return { 'X-MeetingIM-APIKey': API_KEY, Authorization: `Token ${authToken()}` }; } +/** + * Some backend-issued absolute URLs (e.g. Aliyun OSS presigned URLs) come back as `http://` even + * though every page in this app is served over `https://` -- the browser blocks those as mixed + * content before any of our code sees a response. There's no legitimate case where this app needs + * to fetch a plain-http resource, so upgrading the scheme here is safe and has no downside. + */ +export function upgradeToHttps(url: string): string { + return url.startsWith('http://') ? `https://${url.slice('http://'.length)}` : url; +} + /** * Absolute download URL for a file reference returned by uploadFile() or a received attachment. * The upload response's `url` (e.g. "/v0/file/s/file123") is already rooted at the tenant's file @@ -79,7 +89,7 @@ export function fileAuthHeaders(): Record { * fileBase() down to just its origin before appending, rather than concatenating the two paths. */ export function fileDownloadUrl(refUrl: string): string { - if (refUrl.startsWith('http')) return refUrl; + if (refUrl.startsWith('http')) return upgradeToHttps(refUrl); const origin = fileBase().replace(/\/v0\/file.*$/, ''); return `${origin}${refUrl}`; } diff --git a/web/src/lib/tinode/attachments.ts b/web/src/lib/tinode/attachments.ts index aa912e16..07f991b2 100644 --- a/web/src/lib/tinode/attachments.ts +++ b/web/src/lib/tinode/attachments.ts @@ -1,6 +1,6 @@ import { Drafty, type Topic } from 'tinode-sdk'; -import { uploadFile } from '@/lib/api/upload'; +import { uploadFile, upgradeToHttps } from '@/lib/api/upload'; import { MAX_INBAND_ATTACHMENT_SIZE } from '@/lib/config'; import { newClientMid } from '@/lib/tinode/send'; @@ -43,7 +43,7 @@ export async function sendImage(topic: Topic, file: File): Promise { ? Drafty.insertImage(undefined, 0, { mime: file.type, bits: await fileToBase64(file), width, height, filename: file.name, size: file.size }) : Drafty.insertImage(undefined, 0, { mime: file.type, - refurl: (await uploadFile(file, topic.name)).url, + refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), width, height, filename: file.name, @@ -58,7 +58,7 @@ export async function sendFile(topic: Topic, file: File): Promise { const content = file.size <= MAX_INBAND_ATTACHMENT_SIZE ? Drafty.attachFile(undefined, { mime: file.type, data: await fileToBase64(file), filename: file.name, size: file.size }) - : Drafty.attachFile(undefined, { mime: file.type, refurl: (await uploadFile(file, topic.name)).url, filename: file.name, size: file.size }); + : Drafty.attachFile(undefined, { mime: file.type, refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), filename: file.name, size: file.size }); const msg = topic.createMessage(content, false); msg.head = Object.assign(msg.head || {}, { 'x-im-type': 'file', 'x-im-client-mid': newClientMid() }); return topic.publishDraft(msg, Promise.resolve()); @@ -79,7 +79,7 @@ export async function sendVoice(topic: Topic, blob: Blob, durationMs: number): P ? Drafty.insertAudio(undefined, 0, { mime: file.type, bits: await fileToBase64(file), duration: durationMs, size: file.size, filename: file.name }) : Drafty.insertAudio(undefined, 0, { mime: file.type, - refurl: (await uploadFile(file, topic.name)).url, + refurl: upgradeToHttps((await uploadFile(file, topic.name)).url), duration: durationMs, size: file.size, filename: file.name, diff --git a/web/vite.config.ts b/web/vite.config.ts index 5488b850..1ffc1ba1 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ __APP_VERSION__: JSON.stringify(pkg.version), }, server: { - port: 8094, + port: process.env.PORT ? Number(process.env.PORT) : 8094, // Same-origin dev proxy for the REST layer only -- avoids CORS entirely (the browser only ever // sees http://localhost:8094) so a resolved tenant's *relative* endpoints ("/v1/im", "/v0/file") // work unmodified in local dev, same idea as the old app's hand-rolled dev-proxy-server.js, just