UNPKG

@smartsamurai/krapi-sdk

Version:

KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)

636 lines (614 loc) 20.6 kB
/** * Auth Adapter * * Unifies AuthHttpClient and AuthService behind a common interface. * Eliminates mode checks by delegating to the appropriate implementation. */ import { AuthService, ApiKeyAuthRequest } from "../../auth-service"; import { AuthHttpClient } from "../../http-clients/auth-http-client"; import { AdminUser, ProjectUser } from "../../types"; import { handleAdapterError, createAdapterInitError } from "./error-handler"; type Mode = "client" | "server"; export class AuthAdapter { private mode: Mode; private httpClient: AuthHttpClient | undefined; private service: AuthService | undefined; constructor(mode: Mode, httpClient?: AuthHttpClient, service?: AuthService) { this.mode = mode; this.httpClient = httpClient; this.service = service; } async createSession(apiKey: string): Promise<{ session_token: string; expires_at: string; user_type: "admin" | "project"; scopes: string[]; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "createSession"); } try { const response = await this.httpClient.createSession(apiKey); const data = response?.data; if (!data) { throw createAdapterInitError("Response data", this.mode, "No response data from create session"); } return { session_token: data.session_token, expires_at: data.expires_at, user_type: data.user_type, scopes: data.scopes, }; } catch (error) { throw handleAdapterError(error, this.mode, "createSession", { apiKey }); } } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "createSession"); } try { const result = await this.service.createSession({ user_id: apiKey, // API key is used as user_id for API key sessions user_type: "admin", scopes: [], }); return { session_token: result.token, expires_at: result.expires_at, user_type: result.user_type, scopes: result.scopes, }; } catch (error) { throw handleAdapterError(error, this.mode, "createSession", { apiKey }); } } } async login( username: string, password: string, remember_me?: boolean ): Promise<{ session_token: string; expires_at: string; user: AdminUser | ProjectUser; scopes: string[]; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "adminLogin"); } const loginRequest: { username: string; password: string; remember_me?: boolean; } = { username, password, }; if (remember_me !== undefined) loginRequest.remember_me = remember_me; const response = await this.httpClient.adminLogin(loginRequest); const data = response?.data; if (!data) { throw createAdapterInitError("Response data", this.mode, "No response data from admin login"); } return { session_token: data.token, expires_at: data.expires_at, user: data.user as unknown as AdminUser | ProjectUser, scopes: data.scopes, }; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "adminLogin"); } const result = await this.service.authenticateAdmin({ username, password, }); if (!result) { throw createAdapterInitError("Authentication result", this.mode, "No result from authenticate admin"); } return { session_token: result.token, expires_at: result.expires_at, user: result.user as unknown as AdminUser | ProjectUser, scopes: result.scopes, }; } } async setSessionToken(token: string): Promise<void> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "setSessionToken"); } this.httpClient.setSessionToken(token); } else { // Server mode doesn't need session token management // Sessions are managed via database } } setApiKey(apiKey: string): void { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "setApiKey"); } this.httpClient.setApiKey(apiKey); } else { // Server mode doesn't use API keys for HTTP requests } } async logout(): Promise<{ success: boolean }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "logout"); } const response = await this.httpClient.logout(); return response.data || { success: false }; } else { // Server mode logout is handled differently return { success: true }; } } async getCurrentUser(): Promise<{ success: boolean; data?: AdminUser | ProjectUser; error?: string; }> { if (this.mode === "client") { if (!this.httpClient) { return { success: false, error: "HTTP client not initialized", }; } try { const response = await this.httpClient.getCurrentSession(); if (response && typeof response === "object") { if ("success" in response) { const apiResponse = response as { success: boolean; data?: unknown; error?: string }; if (apiResponse.success === false) { return { success: false, error: apiResponse.error || "Failed to get current user", }; } if (apiResponse.data) { const session = apiResponse.data as { user?: unknown }; if (session.user) { return { success: true, data: session.user as AdminUser | ProjectUser, }; } } } if ("user" in response) { return { success: true, data: (response as { user: unknown }).user as AdminUser | ProjectUser, }; } if (("id" in response || "username" in response || "email" in response) && !("success" in response)) { return { success: true, data: response as unknown as AdminUser | ProjectUser, }; } } return { success: false, error: "Invalid response format", }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to get current user", }; } } else { if (!this.service) { return { success: false, error: "Auth service not initialized", }; } // Server mode getCurrentUser requires passing the session token // Use validateSession(token) to get user info in server mode return { success: false, error: "getCurrentUser requires session context. In server mode, use validateSession(token) to get user information.", }; } } async refreshSession(): Promise<{ session_token: string; expires_at: string; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "refreshSession"); } const response = await this.httpClient.refreshSession(); const data = response?.data; if (!data) { throw createAdapterInitError("Response data", this.mode, "No response data from refresh session"); } return { session_token: data.session_token, expires_at: data.expires_at, }; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "refreshSession"); } // Server mode refresh requires the current session token // The caller should pass the token and handle the refresh through the auth service directly throw createAdapterInitError("Session context", this.mode, "refreshSession requires session context. In server mode, use authService.refreshSession(token) directly."); } } async validateSession(token: string): Promise<{ valid: boolean; session?: AdminUser | ProjectUser; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "validateSession"); } const response = await this.httpClient.validateSession(token); const data = response?.data; if (!data) { return { valid: false }; } // HTTP client returns session directly, not wrapped in user object let user: AdminUser | ProjectUser | undefined; if (data.session && typeof data.session === "object") { const sessionObj = data.session as unknown as Record<string, unknown>; // Check if it's AdminUser or ProjectUser based on properties if ("role" in sessionObj || "access_level" in sessionObj) { user = sessionObj as unknown as AdminUser; } else if ("project_id" in sessionObj || "metadata" in sessionObj) { user = sessionObj as unknown as ProjectUser; } } const result: { valid: boolean; session?: AdminUser | ProjectUser } = { valid: data.valid || false, }; if (user) { result.session = user; } return result; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "validateSession"); } const result = await this.service.validateSession(token); if (!result) { const response: { valid: boolean; session?: AdminUser | ProjectUser; } = { valid: false, }; return response; } // Service returns Session | null, but we need to return user object // This is a limitation - the service validateSession doesn't return the user object // We'll return valid: true but session will be undefined until service is updated const response: { valid: boolean; session?: AdminUser | ProjectUser; } = { valid: true, }; return response; } } async changePassword( _oldPassword: string, _newPassword: string ): Promise<{ success: boolean }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "changePassword"); } // Client mode changePassword would need user context throw createAdapterInitError("User context", this.mode, "changePassword requires user context in client mode"); } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "changePassword"); } // Server mode changePassword would need user context throw createAdapterInitError("User context", this.mode, "changePassword requires user context in server mode"); } } async regenerateApiKey( req: unknown ): Promise<{ success: boolean; data?: { apiKey: string }; error?: string }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "regenerateApiKey"); } const response = await this.httpClient.regenerateApiKey(req); return response.data || { success: false, error: "Failed to regenerate API key" }; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "regenerateApiKey"); } return this.service.regenerateApiKey(req); } } // Additional methods for adminLogin wrapper async adminLogin(credentials: { username: string; password: string; remember_me?: boolean; }): Promise<{ success: boolean; data?: { session_token: string; expires_at: string; user: AdminUser | ProjectUser; scopes: string[]; }; error?: string; }> { try { const result = await this.login( credentials.username, credentials.password, credentials.remember_me ); return { success: true, data: { session_token: result.session_token, expires_at: result.expires_at, user: result.user, scopes: result.scopes, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Login failed", }; } } async adminApiLogin( apiKey: string | { api_key?: string } ): Promise<{ success: boolean; data?: { user: AdminUser & { scopes: string[] }; session_token: string; expires_at: string; }; error?: string; }> { if (this.mode === "client") { if (!this.httpClient) { return { success: false, error: "HTTP client not initialized", }; } try { const apiKeyValue = typeof apiKey === "string" ? apiKey : apiKey.api_key; if (!apiKeyValue) { return { success: false, error: "API key is required", }; } const request: ApiKeyAuthRequest = { api_key: apiKeyValue }; const response = await this.httpClient.adminApiLogin(request); // Response interceptor returns response.data, so response is already unwrapped // Handle both formats: { user, token, ... } or { data: { user, token, ... } } let responseData: { user?: unknown; scopes?: string[]; token?: string; expires_at?: string; } | undefined; if (response && typeof response === "object") { // If response has 'data' field, extract it if ("data" in response && response.data && typeof response.data === "object") { responseData = response.data as { user?: unknown; scopes?: string[]; token?: string; expires_at?: string; }; } else { // Response is the data directly responseData = response as { user?: unknown; scopes?: string[]; token?: string; expires_at?: string; }; } } if (responseData && responseData.user) { const userData = responseData.user; const scopes = responseData.scopes || []; return { success: true, data: { user: { ...(userData as Record<string, unknown>), scopes, } as AdminUser & { scopes: string[] }, session_token: responseData.token || "", expires_at: responseData.expires_at || "", }, }; } return { success: false, error: "No data in response", }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "API key login failed", }; } } else { if (!this.service) { return { success: false, error: "Auth service not initialized", }; } try { const apiKeyValue = typeof apiKey === "string" ? apiKey : (apiKey as { api_key?: string }).api_key; if (!apiKeyValue) { return { success: false, error: "API key is required", }; } const result = await this.service.authenticateAdminWithApiKey({ api_key: apiKeyValue, }); const userData = result.user; const scopes = result.scopes || []; return { success: true, data: { user: { ...userData, scopes, } as AdminUser & { scopes: string[] }, session_token: result.token, expires_at: result.expires_at, }, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "API key login failed", }; } } } async register(registerData: { username: string; email: string; password: string; role?: string; access_level?: string; permissions?: string[]; }): Promise<{ success: boolean; user: Record<string, unknown> }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "register"); } const response = await this.httpClient.register(registerData); // Response interceptor returns response.data, so response is already unwrapped // Handle both formats: { success: true, user: {...} } or { success: true, data: { user: {...} } } if (response && typeof response === "object") { // If response has 'data' field, extract it if ("data" in response && response.data && typeof response.data === "object") { const data = response.data as { success?: boolean; user?: Record<string, unknown> }; if (data.user) { return { success: data.success ?? true, user: data.user, }; } } // If response has 'user' field directly if ("user" in response) { const user = (response as { user?: Record<string, unknown> }).user; return { success: (response as { success?: boolean }).success ?? true, user: user || ({} as Record<string, unknown>), }; } } // Fallback to empty user if response format is unexpected return { success: false, user: {} as Record<string, unknown>, }; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "register"); } const result = await this.service.register(registerData); return { success: result.success, user: result.user as unknown as Record<string, unknown>, }; } } async validateApiKey(apiKey: string): Promise<{ valid: boolean; key_info?: { id: string; name: string; type: string; scopes: string[]; project_id?: string; }; }> { if (this.mode === "client") { if (!this.httpClient) { throw createAdapterInitError("HTTP client", this.mode, "validateApiKey"); } const response = await this.httpClient.validateApiKey(apiKey); // Normalize response format if (response && typeof response === "object") { const data = "data" in response ? (response as { data?: unknown }).data : response; if (data && typeof data === "object") { const responseData = data as { valid?: boolean; key_info?: { id?: string; name?: string; type?: string; scopes?: string[]; project_id?: string; }; }; const result: { valid: boolean; key_info?: { id: string; name: string; type: string; scopes: string[]; project_id?: string; }; } = { valid: responseData.valid || false, }; if (responseData.key_info) { result.key_info = { id: responseData.key_info.id || "", name: responseData.key_info.name || "", type: responseData.key_info.type || "", scopes: responseData.key_info.scopes || [], }; if (responseData.key_info.project_id !== undefined) { result.key_info.project_id = responseData.key_info.project_id; } } return result; } } return { valid: false }; } else { if (!this.service) { throw createAdapterInitError("Auth service", this.mode, "validateApiKey"); } // Server mode - use auth service validateApiKey if available // For now, return not implemented since auth service may not have this method throw createAdapterInitError("Auth service", this.mode, "validateApiKey not available in server mode"); } } }