diff --git a/src/auth/client.ts b/src/auth/client.ts index 509204a..3582462 100644 --- a/src/auth/client.ts +++ b/src/auth/client.ts @@ -28,14 +28,14 @@ export class AuthClient { setApiKey(apiKey: string) { this.currentApiKey = apiKey; - this.currentJwt = undefined; + // Don't clear JWT - it will be cleared explicitly on logout this.httpClient.setApiKey(apiKey); this.storage.set("apiKey", apiKey); } setJwt(jwt: string) { this.currentJwt = jwt; - this.currentApiKey = undefined; + // Don't clear API key - keep it as fallback for after logout this.httpClient.setJwt(jwt); this.storage.set("jwt", jwt); } @@ -62,6 +62,51 @@ export class AuthClient { return token; } + /** + * Logout user and clear JWT, but preserve API key + * Use this for user logout in apps where API key is app-level credential + */ + async logoutUser(): Promise { + // Attempt server-side logout if using JWT + if (this.currentJwt) { + try { + await this.httpClient.post("/v1/auth/logout", { all: true }); + } catch (error) { + // Log warning but don't fail - local cleanup is more important + console.warn( + "Server-side logout failed, continuing with local cleanup:", + error + ); + } + } + + // Clear JWT only, preserve API key + this.currentJwt = undefined; + this.httpClient.setJwt(undefined); + await this.storage.set("jwt", ""); // Clear JWT from storage + + // Ensure API key is loaded and set as active auth method + if (!this.currentApiKey) { + // Try to load from storage + const storedApiKey = await this.storage.get("apiKey"); + if (storedApiKey) { + this.currentApiKey = storedApiKey; + } + } + + // Restore API key as the active auth method + if (this.currentApiKey) { + this.httpClient.setApiKey(this.currentApiKey); + console.log("[Auth] API key restored after user logout"); + } else { + console.warn("[Auth] No API key available after logout"); + } + } + + /** + * Full logout - clears both JWT and API key + * Use this to completely reset authentication state + */ async logout(): Promise { // Only attempt server-side logout if using JWT // API keys don't support server-side logout with all=true diff --git a/src/core/http.ts b/src/core/http.ts index 606d5b1..53325f7 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -27,20 +27,53 @@ export class HttpClient { setApiKey(apiKey?: string) { this.apiKey = apiKey; - this.jwt = undefined; + // Don't clear JWT - allow both to coexist + if (typeof console !== "undefined") { + console.log( + "[HttpClient] API key set:", + !!apiKey, + "JWT still present:", + !!this.jwt + ); + } } setJwt(jwt?: string) { this.jwt = jwt; - this.apiKey = undefined; + // Don't clear API key - allow both to coexist + if (typeof console !== "undefined") { + console.log( + "[HttpClient] JWT set:", + !!jwt, + "API key still present:", + !!this.apiKey + ); + } } - private getAuthHeaders(): Record { + private getAuthHeaders(path: string): Record { const headers: Record = {}; - if (this.jwt) { - headers["Authorization"] = `Bearer ${this.jwt}`; - } else if (this.apiKey) { - headers["X-API-Key"] = this.apiKey; + + // For database operations, ONLY use API key to avoid JWT user context + // interfering with namespace-level authorization + const isDbOperation = path.includes("/v1/rqlite/"); + + if (isDbOperation) { + // For database operations: use only API key (preferred for namespace operations) + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } else if (this.jwt) { + // Fallback to JWT if no API key + headers["Authorization"] = `Bearer ${this.jwt}`; + } + } else { + // For auth/other operations: send both JWT and API key + if (this.jwt) { + headers["Authorization"] = `Bearer ${this.jwt}`; + } + if (this.apiKey) { + headers["X-API-Key"] = this.apiKey; + } } return headers; } @@ -68,10 +101,29 @@ export class HttpClient { const headers: Record = { "Content-Type": "application/json", - ...this.getAuthHeaders(), + ...this.getAuthHeaders(path), ...options.headers, }; + // Debug: Log headers being sent + if ( + typeof console !== "undefined" && + (path.includes("/db/") || + path.includes("/query") || + path.includes("/auth/")) + ) { + console.log("[HttpClient] Request headers for", path, { + hasAuth: !!headers["Authorization"], + hasApiKey: !!headers["X-API-Key"], + authPrefix: headers["Authorization"] + ? headers["Authorization"].substring(0, 20) + : "none", + apiKeyPrefix: headers["X-API-Key"] + ? headers["X-API-Key"].substring(0, 20) + : "none", + }); + } + const controller = new AbortController(); const requestTimeout = options.timeout ?? this.timeout; // Use override or default const timeoutId = setTimeout(() => controller.abort(), requestTimeout);