Files
replypier/frontend/src/utils/avatars.ts
T
WangShen007 e24d3e2a65 feat(auth): 后台登录、角色权限与会话访问策略
管理员与客服分角色登录,权限由后端判定;前端补齐登录、人员与组织管理页面。
2026-09-24 21:35:00 +08:00

63 lines
2.7 KiB
TypeScript

/*
Copyright 2024-2026 ChatterMate
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* True when `path` is already an absolute URL (e.g. a signed S3/CDN link) and
* should be used as-is instead of being prefixed with the API base URL.
*
* We test for an absolute http(s) scheme rather than matching a host substring
* such as "amazonaws.com": substring host checks are unreliable — the host can
* appear anywhere in an attacker-influenced string — and are flagged by static
* analysis (CodeQL js/incomplete-url-substring-sanitization).
*/
export const isAbsoluteUrl = (path?: string | null): boolean =>
!!path && (/^https?:\/\//i.test(path) || path.startsWith('data:'))
/**
* Build a browser-usable URL for a stored upload path.
*
* `store_upload` returns local-storage paths that already carry the API prefix
* (`/api/v1/uploads/...`, matching the static mount), so the API base must be
* reduced to its origin before concatenating — otherwise the prefix is emitted
* twice and the request 404s. Absolute URLs (signed S3/CDN links, data URIs,
* blob previews) are returned untouched.
*
* `apiBase` is passed in rather than resolved here because the dashboard and
* the embeddable widget read their API URL from different resolvers with
* different fallbacks; see getApiUrl() and widgetEnv.API_URL.
*/
export const buildUploadUrl = (stored: string | null | undefined, apiBase: string): string => {
if (!stored) return ''
if (isAbsoluteUrl(stored) || stored.startsWith('blob:')) return stored
const origin = apiBase.replace(/\/api\/v1\/?$/, '')
return `${origin}${stored.startsWith('/') ? '' : '/'}${stored}`
}
/**
* Browser URL for a chat attachment in the dashboard.
*
* The API hands attachments out signed — an S3 presigned URL, or a
* `/api/v1/files/local/...?exp=&sig=` link for local storage — and those
* resolve like any other upload. A bare `/uploads/chat_attachments/...` path
* is not publicly served, so it goes through the authenticated download
* endpoint instead.
*/
export const buildAttachmentUrl = (fileUrl: string | null | undefined, apiBase: string): string => {
const unsigned = fileUrl?.match(/^\/?uploads\/(.+)$/)
if (unsigned) return `${apiBase.replace(/\/+$/, '')}/files/download/${unsigned[1]}`
return buildUploadUrl(fileUrl, apiBase)
}