This commit is contained in:
Mateusz Gruszczyński committed 2026-10-14 14:55:04 +02:00
1 parent 9e133628e5
commit 0abc8e871d
6 files changed
+47 -7

No files matched your search

Generated
+1 -1
View File
@@ -2674,7 +2674,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpad" name = "rustpad"
version = "0.2.88" version = "0.2.89"
dependencies = [ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustpad" name = "rustpad"
version = "0.2.88" version = "0.2.89"
edition = "2024" edition = "2024"
rust-version = "1.94" rust-version = "1.94"
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
+4 -3
View File
@@ -1830,9 +1830,10 @@ pub async fn resource_mentions(
.await .await
.map_err(AuthError::database)?; .map_err(AuthError::database)?;
if access.is_none() { if access.is_none() {
return Err(AuthError::forbidden( // A signed-in visitor can read public resources without a direct share.
"This resource was not shared with your account.", // An empty list keeps member identities private and avoids treating
)); // unavailable mention suggestions as a failed page request.
return Ok(Json(serde_json::json!({ "users": [] })));
} }
} }
let rows = sqlx::query(queries::get(state.db.kind(), query)) let rows = sqlx::query(queries::get(state.db.kind(), query))
+3 -1
View File
@@ -164,7 +164,9 @@ export async function api(path, options = {}) {
logWarn("api.timeout", { method: options.method || "GET", path }); logWarn("api.timeout", { method: options.method || "GET", path });
throw new Error(translateApiMessage("Timed out")); throw new Error(translateApiMessage("Timed out"));
} }
logError("api.network_error", error, { method: options.method || "GET", path }); // HTTP failures have already been logged as api.failed above.
// Only transport failures should be reported as network errors.
if (!error?.status) logError("api.network_error", error, { method: options.method || "GET", path });
if (options.body instanceof FormData && error instanceof TypeError) { if (options.body instanceof FormData && error instanceof TypeError) {
throw new Error(translateApiMessage("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit.")); throw new Error(translateApiMessage("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit."));
} }
+3 -1
View File
@@ -277,7 +277,9 @@ export function startNoteEditor(adapter) {
setMarkdownMentions([]); setMarkdownMentions([]);
mentionAutocomplete.setUsers([]); mentionAutocomplete.setUsers([]);
mentionNavigator.setUsers([]); mentionNavigator.setUsers([]);
if (!accountSession) { render(); return; } // Session validation can finish before loadNoteInfo(); avoid a premature
// request (and a second request when note info becomes available).
if (!accountSession || !info) { render(); return; }
try { try {
const params = new URLSearchParams({ kind: adapter.access.kind, slug: adapter.access.key }); const params = new URLSearchParams({ kind: adapter.access.kind, slug: adapter.access.key });
const response = await api(`/api/auth/resources/mentions?${params}`); const response = await api(`/api/auth/resources/mentions?${params}`);
+35
View File
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const events = globalThis.__apiEvents = [];
let source = readFileSync('static/js/api.js', 'utf8')
.replace('import { logDebug, logError, logWarn } from "@rustpad/logger";',
'const logDebug = () => {}; const logError = (...args) => globalThis.__apiEvents.push(["error", ...args]); const logWarn = (...args) => globalThis.__apiEvents.push(["warn", ...args]);')
.replace('import { formatNumber, restoreGuestLanguage, translateApiMessage } from "@rustpad/i18n";',
'const formatNumber = value => String(value); const restoreGuestLanguage = () => {}; const translateApiMessage = message => message;')
.replace('import { restoreGuestTheme } from "@rustpad/theme";',
'const restoreGuestTheme = () => {};');
const { api } = await import('data:text/javascript;charset=utf-8,' + encodeURIComponent(source));
globalThis.window = {};
globalThis.fetch = async () => new Response(JSON.stringify({ error: 'Access denied.' }), {
status: 403,
headers: { 'content-type': 'application/json' },
});
await assert.rejects(api('/api/auth/resources/mentions?kind=pad&slug=test'), error => error.status === 403);
assert.equal(events.filter(([level, name]) => level === 'warn' && name === 'api.failed').length, 1);
assert.equal(events.filter(([level, name]) => level === 'error' && name === 'api.network_error').length, 0);
events.length = 0;
globalThis.fetch = async () => new Response(JSON.stringify({ users: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
assert.deepEqual(await api('/api/auth/resources/mentions?kind=pad&slug=test'), { users: [] });
assert.equal(events.length, 0);
events.length = 0;
globalThis.fetch = async () => { throw new TypeError('Network down'); };
await assert.rejects(api('/api/auth/resources/mentions?kind=pad&slug=test'), TypeError);
assert.equal(events.filter(([level, name]) => level === 'error' && name === 'api.network_error').length, 1);
console.log('PASS: HTTP 403 logged once, empty mentions succeeds, transport failures logged as network errors');