UNPKG

@smartsamurai/krapi-sdk

Version:

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

831 lines (792 loc) 28 kB
/** * Users HTTP Client for KRAPI SDK * * HTTP-based user management methods for frontend applications. * Provides user CRUD operations, role management, and permissions management. * * @module http-clients/users-http-client * @example * const client = new UsersHttpClient({ baseUrl: 'https://api.example.com' }); * const users = await client.getAllUsers('project-id', { limit: 10 }); */ import { QueryOptions } from "../core"; import { ProjectUser } from "../types"; import { ProjectUser as UsersServiceProjectUser } from "../users-service"; import { BaseHttpClient } from "./base-http-client"; import { HttpError } from "./http-error"; /** * Users HTTP Client * * HTTP client for user management operations. * * @class UsersHttpClient * @extends {BaseHttpClient} * @example * const client = new UsersHttpClient({ baseUrl: 'https://api.example.com', apiKey: 'key' }); * const user = await client.createUser('project-id', { username: 'user', email: 'user@example.com', password: 'pass' }); */ export class UsersHttpClient extends BaseHttpClient { // Constructor inherited from BaseHttpClient /** * Get all users in a project * * @param {string} projectId - Project ID * @param {Object} [options] - Query options * @param {string} [options.search] - Search term for username/email * @param {number} [options.limit] - Maximum number of users * @param {number} [options.page] - Page number for pagination * @returns {Promise<ProjectUser[]>} Array of project users * * @example * const users = await client.getAllUsers('project-id', { search: 'john', limit: 10 }); */ async getAllUsers( projectId: string, options?: { search?: string; limit?: number; page?: number; } ): Promise<ProjectUser[]> { try { // BaseHttpClient response interceptor returns response.data, so response is already unwrapped // Backend returns: { success: true, data: [...] } // After interceptor: { success: true, data: [...] } (response.data from axios) const response = await this.get<unknown>( `/projects/${projectId}/users`, options as QueryOptions ); // Normalize response format - handle different backend response formats // Backend can return: { success: true, data: [...] }, { data: [...] }, or directly [...] let users: (UsersServiceProjectUser | ProjectUser)[] = []; if (Array.isArray(response)) { // Direct array response (unlikely but possible) users = response as unknown as ( | UsersServiceProjectUser | ProjectUser )[]; } else if (response && typeof response === "object") { // Check for wrapped format with 'data' key (most common - backend returns { success: true, data: [...] }) if ("data" in response) { const data = (response as { data?: unknown }).data; if (Array.isArray(data)) { users = data as unknown as ( | UsersServiceProjectUser | ProjectUser )[]; } else if (data && typeof data === "object" && "documents" in data) { // Handle nested format: { success: true, data: { documents: [...] } } const nestedData = (data as { documents?: unknown[] }).documents; if (Array.isArray(nestedData)) { users = nestedData as unknown as ( | UsersServiceProjectUser | ProjectUser )[]; } } } // If no data key but has success, might be empty response else if ( "success" in response && (response as { success: boolean }).success ) { // Empty array if success but no data users = []; } } // Map users-service ProjectUser to types.ts ProjectUser (convert is_active to status) return users.map((user) => this.mapToProjectUser(user)); } catch (error) { // Enhance error with context if (error instanceof HttpError) { throw new HttpError( `Failed to fetch users for project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users`, method: error.method || "GET", } ); } throw error; } } /** * Map users-service ProjectUser to types.ts ProjectUser * Converts is_active boolean to status string */ private mapToProjectUser( user: UsersServiceProjectUser | ProjectUser ): ProjectUser { // If already has status, return as-is (already in types.ts format) if ("status" in user && user.status) { return user as ProjectUser; } // Convert from users-service format to types.ts format const serviceUser = user as UsersServiceProjectUser; const mapped: ProjectUser = { id: serviceUser.id, project_id: serviceUser.project_id, username: serviceUser.username || "", email: serviceUser.email || "", role: serviceUser.role as ProjectUser["role"], status: serviceUser.is_active ? "active" : "inactive", created_at: serviceUser.created_at, updated_at: serviceUser.updated_at, permissions: serviceUser.permissions || [], }; // Add optional fields if they exist (only if defined, to avoid undefined assignment) if ( serviceUser.last_login !== undefined && serviceUser.last_login !== null ) { mapped.last_login = serviceUser.last_login; } if ( serviceUser.metadata && typeof serviceUser.metadata === "object" && "phone" in serviceUser.metadata ) { mapped.phone = serviceUser.metadata.phone as string; } return mapped; } /** * Get user by ID * * @param {string} projectId - Project ID * @param {string} userId - User ID * @returns {Promise<ProjectUser>} Project user object * * @example * const user = await client.getUser('project-id', 'user-id'); */ async getUser(projectId: string, userId: string): Promise<ProjectUser> { try { // BaseHttpClient response interceptor returns response.data, so response is already unwrapped // Backend returns: { success: true, data: {...} } // After interceptor: { success: true, data: {...} } (response.data from axios) const response = await this.get<unknown>( `/projects/${projectId}/users/${userId}` ); // Normalize response format - handle different backend response formats // Backend can return: { success: true, data: {...} }, { data: {...} }, or directly {...} let user: UsersServiceProjectUser | ProjectUser | undefined; if (response && typeof response === "object") { // Check if response itself is a ProjectUser (has id, project_id) - direct response if ("id" in response && "project_id" in response) { user = response as unknown as UsersServiceProjectUser | ProjectUser; } // Check for wrapped format with 'data' key (most common - backend returns { success: true, data: {...} }) else if ("data" in response && (response as { data?: unknown }).data) { const data = (response as { data: unknown }).data; if ( data && typeof data === "object" && ("id" in data || "project_id" in data) ) { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } // Check for ApiResponse format with success flag else if ( "success" in response && (response as { success: boolean }).success && "data" in response ) { const data = (response as { data?: unknown }).data; if (data && typeof data === "object") { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } } if (!user) { throw new HttpError( `User not found: ${userId} in project ${projectId}. Response format was unexpected.`, { status: 404, method: "GET", url: `/projects/${projectId}/users/${userId}`, code: "USER_NOT_FOUND", responseData: response, } ); } return this.mapToProjectUser(user); } catch (error) { // Enhance error with context if (error instanceof HttpError) { // If it's already an HttpError with 404, keep it as is if (error.status === 404) { return Promise.reject(error); } // Otherwise enhance it throw new HttpError( `Failed to fetch user ${userId} from project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}`, method: error.method || "GET", } ); } throw error; } } /** * Create a new user in a project * * @param {string} projectId - Project ID * @param {Object} userData - User creation data * @param {string} userData.username - Username * @param {string} userData.email - Email address * @param {string} userData.password - Password * @param {string} [userData.role] - User role (optional) * @param {string[]} [userData.permissions] - User permissions (optional) * @param {string} [userData.first_name] - First name (optional) * @param {string} [userData.last_name] - Last name (optional) * @param {string} [userData.phone] - Phone number (optional) * @returns {Promise<ProjectUser>} Created user object * * @example * const user = await client.createUser('project-id', { * username: 'johndoe', * email: 'john@example.com', * password: 'password123', * role: 'user' * }); */ async createUser( projectId: string, userData: { username: string; email: string; password: string; role?: string; permissions?: string[]; first_name?: string; last_name?: string; phone?: string; } ): Promise<ProjectUser> { try { // Build request body, only including defined properties const requestBody: { username: string; email: string; password: string; role?: string; permissions?: string[]; first_name?: string; last_name?: string; phone?: string; } = { username: userData.username, email: userData.email, password: userData.password, }; if (userData.role !== undefined) { requestBody.role = userData.role; } if (userData.permissions !== undefined) { requestBody.permissions = userData.permissions; } if (userData.first_name !== undefined) { requestBody.first_name = userData.first_name; } if (userData.last_name !== undefined) { requestBody.last_name = userData.last_name; } if (userData.phone !== undefined) { requestBody.phone = userData.phone; } // BaseHttpClient response interceptor returns response.data, so response is already unwrapped // Backend returns: { success: true, data: {...} } or directly {...} const response = await this.post<unknown>( `/projects/${projectId}/users`, requestBody ); // Normalize response format - handle different backend response formats // Backend can return: ProjectUser directly, { success: true, data: {...} }, or { data: {...} } let user: UsersServiceProjectUser | ProjectUser | undefined; if (response && typeof response === "object") { // Check if response itself is a ProjectUser (has id, project_id) if ("id" in response && "project_id" in response) { user = response as unknown as UsersServiceProjectUser | ProjectUser; } // Check for wrapped format with 'data' key (most common) else if ("data" in response && (response as { data?: unknown }).data) { const data = (response as { data: unknown }).data; if ( data && typeof data === "object" && ("id" in data || "project_id" in data) ) { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } // Check for ApiResponse format with success flag else if ( "success" in response && (response as { success: boolean }).success && "data" in response ) { const data = (response as { data?: unknown }).data; if (data && typeof data === "object") { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } } if (!user) { throw new HttpError( `Failed to create user in project ${projectId}: Invalid response format from server. Expected user object but received: ${JSON.stringify( response ).substring(0, 200)}`, { status: 500, method: "POST", url: `/projects/${projectId}/users`, code: "INVALID_RESPONSE", responseData: response, } ); } return this.mapToProjectUser(user); } catch (error) { // Enhance error with context if (error instanceof HttpError) { throw new HttpError( `Failed to create user in project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users`, method: error.method || "POST", } ); } throw error; } } /** * Update a user in a project * * @param {string} projectId - Project ID * @param {string} userId - User ID * @param {Object} updates - User update data * @param {string} [updates.email] - Email address (optional) * @param {string} [updates.username] - Username (optional) * @param {string} [updates.password] - Password (optional) * @param {string} [updates.role] - User role (optional) * @param {string[]} [updates.permissions] - User permissions (optional) * @param {string} [updates.first_name] - First name (optional) * @param {string} [updates.last_name] - Last name (optional) * @param {string} [updates.phone] - Phone number (optional) * @param {boolean} [updates.is_active] - Active status (optional) * @returns {Promise<ProjectUser>} Updated user object * * @example * const user = await client.updateUser('project-id', 'user-id', { * email: 'newemail@example.com', * role: 'admin' * }); */ async updateUser( projectId: string, userId: string, updates: Partial<{ email: string; username: string; password: string; role: string; permissions: string[]; first_name: string; last_name: string; phone: string; is_active: boolean; }> ): Promise<ProjectUser> { try { // BaseHttpClient response interceptor returns response.data, so response is already unwrapped // Backend returns: { success: true, data: {...} } or directly {...} const response = await this.put<unknown>( `/projects/${projectId}/users/${userId}`, updates ); // Normalize response format - handle different backend response formats // Backend can return: ProjectUser directly, { success: true, data: {...} }, or { data: {...} } let user: UsersServiceProjectUser | ProjectUser | undefined; if (response && typeof response === "object") { // Check if response itself is a ProjectUser (has id, project_id) if ("id" in response && "project_id" in response) { user = response as unknown as UsersServiceProjectUser | ProjectUser; } // Check for wrapped format with 'data' key (most common) else if ("data" in response && (response as { data?: unknown }).data) { const data = (response as { data: unknown }).data; if ( data && typeof data === "object" && ("id" in data || "project_id" in data) ) { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } // Check for ApiResponse format with success flag else if ( "success" in response && (response as { success: boolean }).success && "data" in response ) { const data = (response as { data?: unknown }).data; if (data && typeof data === "object") { user = data as unknown as UsersServiceProjectUser | ProjectUser; } } } if (!user) { throw new HttpError( `Failed to update user ${userId} in project ${projectId}: Invalid response format from server. Expected user object but received: ${JSON.stringify( response ).substring(0, 200)}`, { status: 500, method: "PUT", url: `/projects/${projectId}/users/${userId}`, code: "INVALID_RESPONSE", responseData: response, } ); } return this.mapToProjectUser(user); } catch (error: unknown) { // Enhance error with context if (error instanceof HttpError) { throw new HttpError( `Failed to update user ${userId} in project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}`, method: error.method || "PUT", } ); } throw error; } } /** * Delete a user from a project * * @param {string} projectId - Project ID * @param {string} userId - User ID * @returns {Promise<void>} * * @example * await client.deleteUser('project-id', 'user-id'); */ async deleteUser(projectId: string, userId: string): Promise<void> { try { await this.delete<{ success: boolean; message?: string }>( `/projects/${projectId}/users/${userId}` ); // Delete operations typically return void or { success: true } } catch (error) { // Enhance error with context if (error instanceof HttpError) { throw new HttpError( `Failed to delete user ${userId} from project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}`, method: error.method || "DELETE", } ); } throw error; } } /** * Get user scopes * * @param {string} projectId - Project ID * @param {string} userId - User ID * @returns {Promise<string[]>} Array of user scopes * * @example * const scopes = await client.getUserScopes('project-id', 'user-id'); */ async getUserScopes(projectId: string, userId: string): Promise<string[]> { try { const response = await this.get<{ scopes?: string[] }>( `/projects/${projectId}/users/${userId}/scopes` ); // Handle different response formats if (Array.isArray(response)) { return response; } if (response && typeof response === "object" && "scopes" in response) { return (response.scopes as string[]) || []; } if (response && typeof response === "object" && "data" in response) { const data = (response as { data?: unknown }).data; if (Array.isArray(data)) { return data; } if (data && typeof data === "object" && "scopes" in data) { return (data as { scopes?: string[] }).scopes || []; } } return []; } catch (error) { if (error instanceof HttpError) { throw new HttpError( `Failed to get scopes for user ${userId} in project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}/scopes`, method: error.method || "GET", } ); } throw error; } } /** * Update user scopes * * @param {string} projectId - Project ID * @param {string} userId - User ID * @param {string[]} scopes - Array of scopes to set * @returns {Promise<string[]>} Updated scopes * * @example * const updatedScopes = await client.updateUserScopes('project-id', 'user-id', ['data:read', 'data:write']); */ async updateUserScopes( projectId: string, userId: string, scopes: string[] ): Promise<string[]> { try { const response = await this.put<{ scopes?: string[] }>( `/projects/${projectId}/users/${userId}/scopes`, { scopes } ); // Handle different response formats if (Array.isArray(response)) { return response; } if (response && typeof response === "object" && "scopes" in response) { return (response.scopes as string[]) || []; } if (response && typeof response === "object" && "data" in response) { const data = (response as { data?: unknown }).data; if (Array.isArray(data)) { return data; } if (data && typeof data === "object" && "scopes" in data) { return (data as { scopes?: string[] }).scopes || []; } } return scopes; // Return input if response format is unexpected } catch (error) { if (error instanceof HttpError) { throw new HttpError( `Failed to update scopes for user ${userId} in project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}/scopes`, method: error.method || "PUT", } ); } throw error; } } /** * Get user activity log * * @param {string} projectId - Project ID * @param {string} userId - User ID * @param {Object} [options] - Query options * @param {number} [options.limit] - Maximum number of entries * @param {number} [options.offset] - Number of entries to skip * @param {string} [options.start_date] - Start date filter (ISO string) * @param {string} [options.end_date] - End date filter (ISO string) * @returns {Promise<Array<{id: string; action: string; timestamp: string; details: Record<string, unknown>}>>} Array of activity entries * * @example * const activity = await client.getUserActivity('project-id', 'user-id', { limit: 50 }); */ async getUserActivity( projectId: string, userId: string, options?: { limit?: number; offset?: number; start_date?: string; end_date?: string; } ): Promise<Array<{ id: string; action: string; timestamp: string; details: Record<string, unknown>; }>> { try { const response = await this.get<unknown>( `/projects/${projectId}/users/${userId}/activity`, options as QueryOptions ); // Normalize response format let activities: Array<{ id: string; action: string; timestamp: string; details: Record<string, unknown>; }> = []; if (Array.isArray(response)) { activities = response.map((activity: unknown) => { const act = activity as { id?: string; action?: string; created_at?: string; timestamp?: string; details?: Record<string, unknown>; }; return { id: act.id || "", action: act.action || "", timestamp: act.timestamp || act.created_at || new Date().toISOString(), details: act.details || {}, }; }); } else if (response && typeof response === "object" && "data" in response) { const data = (response as { data?: unknown }).data; if (Array.isArray(data)) { activities = data.map((activity: unknown) => { const act = activity as { id?: string; action?: string; created_at?: string; timestamp?: string; details?: Record<string, unknown>; }; return { id: act.id || "", action: act.action || "", timestamp: act.timestamp || act.created_at || new Date().toISOString(), details: act.details || {}, }; }); } } return activities; } catch (error) { if (error instanceof HttpError) { throw new HttpError( `Failed to get activity for user ${userId} in project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/${userId}/activity`, method: error.method || "GET", } ); } throw error; } } /** * Get user statistics for a project * * @param {string} projectId - Project ID * @returns {Promise<{total_users: number; active_users: number; users_by_role: Record<string, number>; recent_logins: number}>} User statistics * * @example * const stats = await client.getUserStatistics('project-id'); */ async getUserStatistics(projectId: string): Promise<{ total_users: number; active_users: number; users_by_role: Record<string, number>; recent_logins: number; }> { try { const response = await this.get<unknown>( `/projects/${projectId}/users/statistics` ); // Normalize response format let stats: { total_users: number; active_users: number; users_by_role: Record<string, number>; recent_logins: number; }; if (response && typeof response === "object") { // Check if response itself has the stats if ("total_users" in response || "data" in response) { const data = "data" in response ? (response as { data?: unknown }).data : response; if (data && typeof data === "object") { const statsData = data as { total_users?: number; active_users?: number; users_by_role?: Record<string, number>; recent_logins?: number; }; stats = { total_users: statsData.total_users || 0, active_users: statsData.active_users || 0, users_by_role: statsData.users_by_role || {}, recent_logins: statsData.recent_logins || 0, }; } else { throw new HttpError( `Invalid response format from server for user statistics`, { status: 500, method: "GET", url: `/projects/${projectId}/users/statistics`, code: "INVALID_RESPONSE", responseData: response, } ); } } else { throw new HttpError( `Invalid response format from server for user statistics`, { status: 500, method: "GET", url: `/projects/${projectId}/users/statistics`, code: "INVALID_RESPONSE", responseData: response, } ); } } else { throw new HttpError( `Invalid response format from server for user statistics`, { status: 500, method: "GET", url: `/projects/${projectId}/users/statistics`, code: "INVALID_RESPONSE", responseData: response, } ); } return stats; } catch (error) { if (error instanceof HttpError) { throw new HttpError( `Failed to get user statistics for project ${projectId}: ${error.message}`, { ...error, url: error.url || `/projects/${projectId}/users/statistics`, method: error.method || "GET", } ); } throw error; } } }