mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-08 23:32:40 +02:00
feat: enhance authentication handling and UI feedback in SurfSense plugin
- Introduced an authentication block mechanism to prevent repeated invalid token submissions. - Updated notification logic to provide clearer feedback on API token status. - Refactored status visuals for better clarity and consistency in the user interface. - Improved connection handling in the sync engine to ensure robust error management.
This commit is contained in:
parent
e84dc87c5b
commit
1c18735d38
7 changed files with 42 additions and 49 deletions
|
|
@ -159,7 +159,7 @@ async def _extract_binary_attachment_markdown(
|
|||
logger.warning("obsidian attachment payload had invalid base64: %s", payload.path)
|
||||
return "", {"attachment_extraction_status": "invalid_binary_payload"}
|
||||
|
||||
suffix = f".{payload.extension.lstrip('.')}" if payload.extension else ""
|
||||
suffix = f".{payload.extension.lstrip('.')}"
|
||||
temp_path: str | None = None
|
||||
filename = payload.path.rsplit("/", 1)[-1] or payload.name
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "obsidian";
|
||||
import { requestUrl, type RequestUrlParam, type RequestUrlResponse } from "obsidian";
|
||||
import type {
|
||||
ConnectResponse,
|
||||
DeleteAck,
|
||||
|
|
@ -72,8 +72,11 @@ export interface ApiClientOptions {
|
|||
onAuthError?: () => void;
|
||||
}
|
||||
|
||||
const AUTH_BLOCK_MS = 60_000;
|
||||
|
||||
export class SurfSenseApiClient {
|
||||
private readonly opts: ApiClientOptions;
|
||||
private authBlockedUntil = 0;
|
||||
|
||||
constructor(opts: ApiClientOptions) {
|
||||
this.opts = opts;
|
||||
|
|
@ -83,6 +86,10 @@ export class SurfSenseApiClient {
|
|||
Object.assign(this.opts, partial);
|
||||
}
|
||||
|
||||
resetAuthBlock(): void {
|
||||
this.authBlockedUntil = 0;
|
||||
}
|
||||
|
||||
async health(): Promise<HealthResponse> {
|
||||
return await this.request<HealthResponse>("GET", "/api/v1/obsidian/health");
|
||||
}
|
||||
|
|
@ -198,6 +205,9 @@ export class SurfSenseApiClient {
|
|||
if (!token) {
|
||||
throw new AuthError("Missing API token. Open SurfSense settings to paste one.");
|
||||
}
|
||||
if (Date.now() < this.authBlockedUntil) {
|
||||
throw new AuthError("Token rejected. Paste a fresh one in settings.");
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: "application/json",
|
||||
|
|
@ -224,8 +234,8 @@ export class SurfSenseApiClient {
|
|||
const detail = extractDetail(resp);
|
||||
|
||||
if (resp.status === 401) {
|
||||
this.authBlockedUntil = Date.now() + AUTH_BLOCK_MS;
|
||||
this.opts.onAuthError?.();
|
||||
new Notice("Surfsense: token expired or invalid. Paste a fresh token in settings.");
|
||||
throw new AuthError(detail || "Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
private settingTab: SurfSenseSettingTab | null = null;
|
||||
private statusListeners = new Set<() => void>();
|
||||
private reconcileTimerId: number | null = null;
|
||||
private lastAuthToastAt = 0;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -34,6 +35,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
this.api = new SurfSenseApiClient({
|
||||
getServerUrl: () => this.settings.serverUrl,
|
||||
getToken: () => this.settings.apiToken,
|
||||
onAuthError: () => this.notifyAuthError(),
|
||||
});
|
||||
|
||||
this.queue = new PersistentQueue(this.settings.queue ?? [], {
|
||||
|
|
@ -239,6 +241,13 @@ export default class SurfSensePlugin extends Plugin {
|
|||
for (const fn of this.statusListeners) fn();
|
||||
}
|
||||
|
||||
private notifyAuthError(): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastAuthToastAt < 10_000) return;
|
||||
this.lastAuthToastAt = now;
|
||||
new Notice("Surfsense: API token expired or invalid. Paste a fresh token in settings.", 8000);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null;
|
||||
this.settings = {
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}
|
||||
this.plugin.settings.apiToken = next;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.api.resetAuthBlock();
|
||||
});
|
||||
})
|
||||
.addButton((btn) =>
|
||||
|
|
@ -299,7 +300,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
const kind = this.plugin.lastStatus.kind;
|
||||
|
||||
if (kind === "auth-error") {
|
||||
return { icon: "lock", label: "Token invalid or expired", tone: "err" };
|
||||
return { icon: "lock", label: "API token invalid or expired", tone: "err" };
|
||||
}
|
||||
if (kind === "error") {
|
||||
return { icon: "alert-circle", label: "Connection error", tone: "err" };
|
||||
|
|
@ -381,10 +382,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private handleApiError(err: unknown): void {
|
||||
if (err instanceof AuthError) {
|
||||
new Notice(`SurfSense: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
if (err instanceof AuthError) return;
|
||||
new Notice(
|
||||
`SurfSense: request failed — ${(err as Error).message ?? "unknown error"}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ interface StatusVisual {
|
|||
}
|
||||
|
||||
const VISUALS: Record<StatusKind, StatusVisual> = {
|
||||
idle: { icon: "check-circle", label: "Synced", cls: "surfsense-status--ok" },
|
||||
syncing: { icon: "refresh-ccw", label: "Syncing", cls: "surfsense-status--syncing" },
|
||||
queued: { icon: "upload", label: "Queued", cls: "surfsense-status--syncing" },
|
||||
offline: { icon: "wifi-off", label: "Offline", cls: "surfsense-status--warn" },
|
||||
"auth-error": { icon: "lock", label: "Auth error", cls: "surfsense-status--err" },
|
||||
idle: { icon: "check-circle", label: "Synced", cls: "" },
|
||||
syncing: { icon: "refresh-ccw", label: "Syncing", cls: "" },
|
||||
queued: { icon: "clock", label: "Queued", cls: "" },
|
||||
offline: { icon: "wifi-off", label: "Offline", cls: "" },
|
||||
"auth-error": { icon: "user-x", label: "Auth error", cls: "surfsense-status--err" },
|
||||
error: { icon: "alert-circle", label: "Error", cls: "surfsense-status--err" },
|
||||
};
|
||||
|
||||
|
|
@ -42,13 +42,8 @@ export class StatusBar {
|
|||
|
||||
update(state: StatusState): void {
|
||||
const visual = VISUALS[state.kind];
|
||||
this.el.removeClass(
|
||||
"surfsense-status--ok",
|
||||
"surfsense-status--syncing",
|
||||
"surfsense-status--warn",
|
||||
"surfsense-status--err",
|
||||
);
|
||||
this.el.addClass(visual.cls);
|
||||
this.el.removeClass("surfsense-status--err");
|
||||
if (visual.cls) this.el.addClass(visual.cls);
|
||||
setIcon(this.icon, visual.icon);
|
||||
|
||||
let label = `SurfSense: ${visual.label}`;
|
||||
|
|
|
|||
|
|
@ -120,11 +120,11 @@ export class SyncEngine {
|
|||
* (Re)register the vault. Adopts server's `vault_id` in case fingerprint
|
||||
* dedup routed us to an existing row from another device.
|
||||
*/
|
||||
async ensureConnected(): Promise<void> {
|
||||
async ensureConnected(): Promise<boolean> {
|
||||
const settings = this.deps.getSettings();
|
||||
if (!settings.searchSpaceId) {
|
||||
this.setStatus("idle", "Pick a search space in settings.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.setStatus("syncing", "Connecting to SurfSense");
|
||||
try {
|
||||
|
|
@ -141,8 +141,10 @@ export class SyncEngine {
|
|||
s.connectorId = resp.connector_id;
|
||||
});
|
||||
this.setStatus(this.queueStatusKind(), this.statusDetail());
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.handleStartupError(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +240,8 @@ export class SyncEngine {
|
|||
if (this.deps.queue.size === 0) return;
|
||||
// Shared gate for every flush trigger so the first /sync can't race /connect.
|
||||
if (!this.deps.getSettings().connectorId) {
|
||||
await this.ensureConnected();
|
||||
const connected = await this.ensureConnected();
|
||||
if (!connected) return;
|
||||
if (!this.deps.getSettings().connectorId) return;
|
||||
}
|
||||
this.setStatus("syncing", `Syncing ${this.deps.queue.size} item(s)…`);
|
||||
|
|
@ -409,7 +412,8 @@ export class SyncEngine {
|
|||
// Re-handshake first: if the vault grew enough to match another
|
||||
// device's fingerprint, the server merges and routes us to the
|
||||
// survivor row, which the /manifest call below then uses.
|
||||
await this.ensureConnected();
|
||||
const connected = await this.ensureConnected();
|
||||
if (!connected) return;
|
||||
const refreshed = this.deps.getSettings();
|
||||
if (!refreshed.connectorId) return;
|
||||
|
||||
|
|
@ -552,7 +556,8 @@ export class SyncEngine {
|
|||
}
|
||||
|
||||
private classifyAndStatus(err: unknown, prefix: string): void {
|
||||
this.classify(err);
|
||||
const verdict = this.classify(err);
|
||||
if (verdict === "stop") return;
|
||||
this.setStatus(this.queueStatusKind(), `${prefix}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,18 +22,6 @@
|
|||
height: 14px;
|
||||
}
|
||||
|
||||
.surfsense-status--ok .surfsense-status__icon {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.surfsense-status--syncing .surfsense-status__icon {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.surfsense-status--warn .surfsense-status__icon {
|
||||
color: var(--color-yellow);
|
||||
}
|
||||
|
||||
.surfsense-status--err .surfsense-status__icon {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
|
@ -55,18 +43,6 @@
|
|||
height: 14px;
|
||||
}
|
||||
|
||||
.surfsense-connection-indicator--ok {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.surfsense-connection-indicator--syncing {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.surfsense-connection-indicator--warn {
|
||||
color: var(--color-yellow);
|
||||
}
|
||||
|
||||
.surfsense-connection-indicator--err {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue