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