2 Commits
Author SHA1 Message Date
justme c68acfd78b fix: allow embedded worker API requests
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/tag/release-backend Pipeline was successful
2026-10-02 14:12:14 +08:00
justme 10ea48bf04 fix: clarify quick share creation failures
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/tag/release-backend Pipeline was successful
2026-10-02 13:44:03 +08:00
4 changed files with 15 additions and 10 deletions

No files matched your search

@@ -87,7 +87,9 @@ public static class EmbeddedFrontend
? "public, max-age=31536000, immutable" ? "public, max-age=31536000, immutable"
: "no-store"; : "no-store";
if (path.Equals("index.html", StringComparison.Ordinal)) // Dedicated workers enforce the CSP returned with their own script response.
// Give bundled JS the same app policy so the crypto worker can call the same-origin API.
if (path.Equals("index.html", StringComparison.Ordinal) || path.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
context.Response.Headers["Content-Security-Policy"] = contentSecurityPolicy; context.Response.Headers["Content-Security-Policy"] = contentSecurityPolicy;
if (!HttpMethods.IsHead(context.Request.Method)) if (!HttpMethods.IsHead(context.Request.Method))
+5 -2
View File
@@ -1,7 +1,7 @@
import { supportsApiContract } from '../contract'; import { supportsApiContract } from '../contract';
export type ApiError = Error & { status?: number; code?: string }; export type ApiError = Error & { status?: number; code?: string };
let apiBase = '/api/v1'; let apiBase = new URL('/api/v1', self.location.origin).toString();
let contractCheck: Promise<void> | undefined; let contractCheck: Promise<void> | undefined;
export function setApiBase(value: string): void { export function setApiBase(value: string): void {
@@ -11,7 +11,10 @@ export function setApiBase(value: string): void {
} }
export const fail = (status: number, code: string): ApiError => Object.assign(new Error(code), { status, code }); export const fail = (status: number, code: string): ApiError => Object.assign(new Error(code), { status, code });
export const apiPath = (path: string): string => `${apiBase.replace(/\/$/, '')}${path}`; export const apiPath = (path: string): string => new URL(
`${apiBase.replace(/\/$/, '')}${path}`,
self.location.origin,
).toString();
function ensureApiContract(): Promise<void> { function ensureApiContract(): Promise<void> {
contractCheck ??= fetch(apiPath('/info'), { contractCheck ??= fetch(apiPath('/info'), {
+6 -5
View File
@@ -6,6 +6,7 @@ import { useGlobalPaste } from '@/lib/useGlobalPaste';
import { useChannels } from '@/store/channels'; import { useChannels } from '@/store/channels';
import { useShares } from '@/store/shares'; import { useShares } from '@/store/shares';
import { solveCreationCaptcha } from '@/core-bridge/captcha'; import { solveCreationCaptcha } from '@/core-bridge/captcha';
import { errorMessage } from '@/core-bridge/errors';
import { Button } from '@/ui/Button'; import { Button } from '@/ui/Button';
import { ChannelList } from './ChannelList'; import { ChannelList } from './ChannelList';
import { CreateSheet } from './CreateSheet'; import { CreateSheet } from './CreateSheet';
@@ -22,7 +23,7 @@ export function Home() {
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [scanOpen, setScanOpen] = useState(false); const [scanOpen, setScanOpen] = useState(false);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [failed, setFailed] = useState(false); const [failedMessage, setFailedMessage] = useState('');
const [shareText, setShareText] = useState(''); const [shareText, setShareText] = useState('');
const [shareFiles, setShareFiles] = useState<File[]>([]); const [shareFiles, setShareFiles] = useState<File[]>([]);
@@ -32,15 +33,15 @@ export function Home() {
async (p: ClipboardPayload) => { async (p: ClipboardPayload) => {
if (busy || (p.files.length === 0 && !p.text.trim())) return false; if (busy || (p.files.length === 0 && !p.text.trim())) return false;
setBusy(true); setBusy(true);
setFailed(false); setFailedMessage('');
try { try {
const captchaToken = await solveCreationCaptcha(); const captchaToken = await solveCreationCaptcha();
open(await createDraft(p, captchaToken)); open(await createDraft(p, captchaToken));
setShareFiles([]); setShareFiles([]);
setShareText(''); setShareText('');
return true; return true;
} catch { } catch (error) {
setFailed(true); setFailedMessage(errorMessage(error));
return false; return false;
} finally { } finally {
setBusy(false); setBusy(false);
@@ -71,7 +72,7 @@ export function Home() {
onFilesChange={setShareFiles} onFilesChange={setShareFiles}
onShare={share} onShare={share}
/> />
{failed ? <p role="alert" className={styles.error}>创建分享失败,请重试。</p> : null} {failedMessage ? <p role="alert" className={styles.error}>创建分享失败:{failedMessage}</p> : null}
<ChannelList kind="share" title="我的分享" onOpen={open} /> <ChannelList kind="share" title="我的分享" onOpen={open} />
+1 -2
View File
@@ -58,8 +58,7 @@
} }
.editor:focus-visible { .editor:focus-visible {
outline: 2px solid var(--accent); outline: none;
outline-offset: 2px;
} }
.editor::placeholder { .editor::placeholder {