UNPKG

@smartsamurai/krapi-sdk

Version:

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

1,526 lines (1,521 loc) 850 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/core/krapi-error.ts function maskSensitiveValue(value) { if (typeof value === "string") { if (value.length > 10 && (value.toLowerCase().includes("password") || value.toLowerCase().includes("token") || value.toLowerCase().includes("secret") || value.toLowerCase().includes("key"))) { return `${value.substring(0, 4)}****`; } if (value.length > 50) { return `${value.substring(0, 20)}...[${value.length - 40} chars]...${value.substring(value.length - 20)}`; } } return value; } function getErrorCodeFromMessage(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) { return "FORBIDDEN"; } if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) { return "NOT_FOUND"; } if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) { return "CONFLICT"; } if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) { return "VALIDATION_ERROR"; } if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) { return "NETWORK_ERROR"; } if (lowerMessage.includes("rate limit")) { return "RATE_LIMIT_EXCEEDED"; } if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) { return "INTERNAL_ERROR"; } if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) { return "SERVICE_UNAVAILABLE"; } if (lowerMessage.includes("bad request")) { return "BAD_REQUEST"; } return void 0; } var KrapiError; var init_krapi_error = __esm({ "src/core/krapi-error.ts"() { "use strict"; KrapiError = class _KrapiError extends Error { /** * Create a new KrapiError instance * * @param {string} message - Error message * @param {ErrorCode} [code] - Error code * @param {number} [status] - HTTP status code * @param {Record<string, unknown>} [details] - Additional error details * @param {string} [requestId] - Request ID for tracking * @param {unknown} [cause] - Original error that caused this error */ constructor(message, code = "INTERNAL_ERROR", status, details, requestId, cause) { super(message); this.name = "KrapiError"; this.code = code; if (status !== void 0) this.status = status; if (details !== void 0) this.details = details; if (requestId !== void 0) this.requestId = requestId; if (cause !== void 0) this.cause = cause; this.timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (Error.captureStackTrace) { Error.captureStackTrace(this, _KrapiError); } } // Static factory methods /** * Create KrapiError from HttpError * * @param httpError - HttpError instance to convert * @param context - Additional context to include * @returns New KrapiError instance */ static fromHttpError(httpError, context) { let errorCode = "INTERNAL_ERROR"; if (httpError.status) { switch (httpError.status) { case 400: errorCode = "BAD_REQUEST"; break; case 401: errorCode = "UNAUTHORIZED"; break; case 403: errorCode = "FORBIDDEN"; break; case 404: errorCode = "NOT_FOUND"; break; case 409: errorCode = "CONFLICT"; break; case 422: errorCode = "UNPROCESSABLE_ENTITY"; break; case 429: errorCode = "RATE_LIMIT_EXCEEDED"; break; case 500: errorCode = "SERVER_ERROR"; break; case 502: case 503: errorCode = "SERVICE_UNAVAILABLE"; break; case 408: errorCode = "TIMEOUT"; break; } } return new _KrapiError( httpError.message, errorCode, httpError.status, { httpError: { code: httpError.code, responseData: httpError.responseData }, ...context } ); } /** * Create KrapiError from generic Error * * @param error - Generic error to convert * @param defaultCode - Default error code if cannot be inferred * @param context - Additional context to include * @returns New KrapiError instance */ static fromError(error, defaultCode = "INTERNAL_ERROR", context) { const code = getErrorCodeFromMessage(error.message) || defaultCode; return new _KrapiError( error.message, code, void 0, { originalStack: error.stack, ...context }, void 0, error ); } /** * Create validation error * * @param message - Error message * @param field - Field that failed validation * @param value - Invalid value (will be masked for security) * @param context - Additional context * @returns New validation error */ static validationError(message, field, value, context) { return new _KrapiError( message, "VALIDATION_ERROR", 400, { field, value: maskSensitiveValue(value), ...context } ); } /** * Create not found error * * @param message - Error message or resource type * @param context - Additional context (optional) * @returns New not found error */ static notFound(message, context) { return new _KrapiError( message, "NOT_FOUND", 404, context ); } /** * Create authentication error * * @param message - Error message * @param context - Additional context * @returns New authentication error */ static authError(message = "Authentication required", context) { return new _KrapiError( message, "UNAUTHORIZED", 401, context ); } /** * Create authorization error * * @param message - Error message * @param context - Additional context * @returns New authorization error */ static forbidden(message = "Access forbidden", context) { return new _KrapiError( message, "FORBIDDEN", 403, context ); } /** * Create conflict error * * @param message - Error message * @param context - Additional context * @returns New conflict error */ static conflict(message, context) { return new _KrapiError( message, "CONFLICT", 409, context ); } /** * Create internal server error * * @param message - Error message * @param context - Additional context * @returns New internal server error */ static internalError(message = "Internal server error", context) { return new _KrapiError( message, "INTERNAL_ERROR", 500, context ); } /** * Create bad request error * * @param message - Error message * @param context - Additional context * @returns New bad request error */ static badRequest(message, context) { return new _KrapiError( message, "BAD_REQUEST", 400, context ); } /** * Create service unavailable error * * @param message - Error message * @param context - Additional context * @returns New service unavailable error */ static serviceUnavailable(message = "Service unavailable", context) { return new _KrapiError( message, "SERVICE_UNAVAILABLE", 503, context ); } // Instance methods /** * Create a new error with additional context * * @param context - Additional context to merge * @returns New KrapiError with merged context */ withContext(context) { return new _KrapiError( this.message, this.code, this.status, { ...this.details || {}, ...context }, this.requestId, this.cause ); } /** * Check if this is a validation error * * @returns True if this is a validation error */ isValidationError() { return this.code === "VALIDATION_ERROR"; } /** * Check if this is a not found error * * @returns True if this is a not found error */ isNotFound() { return this.code === "NOT_FOUND"; } /** * Check if this is an authentication error * * @returns True if this is an authentication error */ isAuthError() { return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN"; } /** * Check if this is a client error (4xx) * * @returns True if this is a client error */ isClientError() { return this.status !== void 0 && this.status >= 400 && this.status < 500; } /** * Check if this is a server error (5xx) * * @returns True if this is a server error */ isServerError() { return this.status !== void 0 && this.status >= 500; } /** * Convert error to JSON format * * @returns {Object} Error as JSON object */ toJSON() { const result = { code: this.code, message: this.message, timestamp: this.timestamp }; if (this.status !== void 0) result.status = this.status; if (this.details !== void 0) result.details = this.details; if (this.requestId !== void 0) result.request_id = this.requestId; if (this.cause !== void 0) { result.cause = this.cause instanceof Error ? this.cause.message : String(this.cause); } return result; } /** * Get detailed error message * * @returns {string} Detailed error message */ getDetailedMessage() { let message = this.message; if (this.code) { message = `[${this.code}] ${message}`; } if (this.status) { message = `${message} (HTTP ${this.status})`; } return message; } }; } }); // src/http-clients/http-error.ts var HttpError; var init_http_error = __esm({ "src/http-clients/http-error.ts"() { "use strict"; HttpError = class _HttpError extends Error { /** * Create a new HttpError instance * * @param {string} message - Error message * @param {Object} options - Error options * @param {number} [options.status] - HTTP status code * @param {string} [options.method] - HTTP method * @param {string} [options.url] - Request URL * @param {Record<string, string>} [options.requestHeaders] - Request headers * @param {unknown} [options.requestBody] - Request body/data sent * @param {Record<string, unknown>} [options.requestQuery] - Request query parameters * @param {unknown} [options.responseData] - Response data * @param {Record<string, string>} [options.responseHeaders] - Response headers * @param {string} [options.code] - Error code * @param {unknown} [options.originalError] - Original error */ constructor(message, options = {}) { super(message); this.name = "HttpError"; if (options.status !== void 0) { this.status = options.status; } if (options.method !== void 0) { this.method = options.method; } if (options.url !== void 0) { this.url = options.url; } if (options.requestHeaders !== void 0) { this.requestHeaders = options.requestHeaders; } if (options.requestBody !== void 0) { this.requestBody = options.requestBody; } if (options.requestQuery !== void 0) { this.requestQuery = options.requestQuery; } if (options.responseData !== void 0) { this.responseData = options.responseData; } if (options.responseHeaders !== void 0) { this.responseHeaders = options.responseHeaders; } if (options.code !== void 0) { this.code = options.code; } if (options.originalError !== void 0) { this.originalError = options.originalError; } this.isApiError = options.status !== void 0 && options.status >= 400; this.isNetworkError = options.status === void 0; this.isAuthError = options.status === 401 || options.status === 403; this.isClientError = options.status !== void 0 && options.status >= 400 && options.status < 500; this.isServerError = options.status !== void 0 && options.status >= 500; if (Error.captureStackTrace) { Error.captureStackTrace(this, _HttpError); } } /** * Get a detailed error message with all available information * * @returns {string} Detailed error message */ getDetailedMessage() { const parts = [this.message]; if (this.status) { parts.push(`(HTTP ${this.status})`); } if (this.method && this.url) { parts.push(`[${this.method} ${this.url}]`); } if (this.code) { parts.push(`[Code: ${this.code}]`); } if (this.responseData && typeof this.responseData === "object") { const response = this.responseData; if (response.error && typeof response.error === "string") { parts.push(`Backend error: ${response.error}`); } else if (response.message && typeof response.message === "string") { parts.push(`Backend message: ${response.message}`); } } return parts.join(" "); } /** * Convert error to JSON for logging * * @returns {Record<string, unknown>} JSON representation */ toJSON() { return { name: this.name, message: this.message, status: this.status, method: this.method, url: this.url, code: this.code, isApiError: this.isApiError, isNetworkError: this.isNetworkError, isAuthError: this.isAuthError, isClientError: this.isClientError, isServerError: this.isServerError, requestHeaders: this.requestHeaders, requestBody: this.requestBody, requestQuery: this.requestQuery, responseData: this.responseData, responseHeaders: this.responseHeaders, originalError: this.originalError, stack: this.stack }; } }; } }); // src/utils/error-handler.ts var error_handler_exports = {}; __export(error_handler_exports, { createRequestId: () => createRequestId, enrichError: () => enrichError, getErrorCodeFromMessage: () => getErrorCodeFromMessage2, getErrorCodeFromStatus: () => getErrorCodeFromStatus, normalizeError: () => normalizeError, preserveErrorContext: () => preserveErrorContext, transformHttpError: () => transformHttpError }); function normalizeError(error, defaultCode = "INTERNAL_ERROR", context) { if (error instanceof KrapiError) { return context ? enrichError(error, context) : error; } if (error instanceof HttpError) { return transformHttpError(error, context); } if (error instanceof Error) { const code = getErrorCodeFromMessage2(error.message); return new KrapiError( error.message, code, void 0, { ...context, originalError: error, stack: error.stack } ); } if (typeof error === "string") { const code = getErrorCodeFromMessage2(error); return new KrapiError( error, code, void 0, context ); } return new KrapiError( "An unexpected error occurred", defaultCode, void 0, { ...context, originalError: error } ); } function transformHttpError(httpError, context) { const errorCode = getErrorCodeFromStatus(httpError.status); let message = httpError.message; if (httpError.status && !message.includes(`HTTP ${httpError.status}`)) { message = `${message} (HTTP ${httpError.status})`; } const krapiError = new KrapiError( message, errorCode, httpError.status, { ...context, httpError: { method: httpError.method, url: httpError.url, status: httpError.status, code: httpError.code, isApiError: httpError.isApiError, isNetworkError: httpError.isNetworkError, isAuthError: httpError.isAuthError, isClientError: httpError.isClientError, isServerError: httpError.isServerError, // Request context - what was sent requestHeaders: httpError.requestHeaders, requestBody: httpError.requestBody, requestQuery: httpError.requestQuery, // Response context - what was received responseData: httpError.responseData, responseHeaders: httpError.responseHeaders }, originalError: httpError } ); return krapiError; } function enrichError(error, context) { const mergedDetails = { ...error.details || {}, ...context }; if (error.details?.originalError) { mergedDetails.originalError = error.details.originalError; } return new KrapiError( error.message, error.code, error.status, mergedDetails, error.requestId, error.cause ); } function getErrorCodeFromStatus(status) { if (!status) { return "NETWORK_ERROR"; } switch (status) { case 400: return "BAD_REQUEST"; case 401: return "UNAUTHORIZED"; case 403: return "FORBIDDEN"; case 404: return "NOT_FOUND"; case 409: return "CONFLICT"; case 422: return "UNPROCESSABLE_ENTITY"; case 429: return "RATE_LIMIT_EXCEEDED"; case 500: return "SERVER_ERROR"; case 502: case 503: case 504: return "SERVICE_UNAVAILABLE"; case 408: return "TIMEOUT"; default: if (status >= 400 && status < 500) { return "BAD_REQUEST"; } if (status >= 500) { return "SERVER_ERROR"; } return "INTERNAL_ERROR"; } } function getErrorCodeFromMessage2(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) { return "FORBIDDEN"; } if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) { return "UNAUTHORIZED"; } if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) { return "NOT_FOUND"; } if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) { return "CONFLICT"; } if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) { return "VALIDATION_ERROR"; } if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) { return "NETWORK_ERROR"; } if (lowerMessage.includes("rate limit")) { return "RATE_LIMIT_EXCEEDED"; } if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) { return "INTERNAL_ERROR"; } if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) { return "SERVICE_UNAVAILABLE"; } if (lowerMessage.includes("bad request")) { return "BAD_REQUEST"; } return "INTERNAL_ERROR"; } function preserveErrorContext(error, additionalContext) { if (error instanceof KrapiError) { return additionalContext ? enrichError(error, additionalContext) : error; } return normalizeError(error, void 0, additionalContext); } function createRequestId() { return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } var init_error_handler = __esm({ "src/utils/error-handler.ts"() { "use strict"; init_krapi_error(); init_http_error(); } }); // src/utils/error-logger.ts function logError(logger, error, context, level = "error") { const logEntry = createErrorLogEntry(error, context); switch (level) { case "error": logger.error(formatErrorMessage(logEntry), logEntry); break; case "warn": logger.warn(formatErrorMessage(logEntry), logEntry); break; case "info": logger.info(formatErrorMessage(logEntry), logEntry); break; case "debug": logger.debug(formatErrorMessage(logEntry), logEntry); break; } } function createErrorLogEntry(error, context) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); if (error instanceof HttpError) { const httpContext = { ...context || {}, ...error.method ? { httpMethod: error.method } : {}, ...error.url ? { httpUrl: error.url } : {}, ...error.status !== void 0 ? { httpStatus: error.status } : {}, ...error.requestHeaders ? { requestHeaders: error.requestHeaders } : {}, ...error.requestBody !== void 0 ? { requestBody: error.requestBody } : {}, ...error.requestQuery ? { requestQuery: error.requestQuery } : {}, ...error.responseData !== void 0 ? { responseData: error.responseData } : {}, ...error.responseHeaders ? { responseHeaders: error.responseHeaders } : {} }; const logEntry2 = { timestamp, level: "error", message: error.message, error: { code: error.code || "HTTP_ERROR", message: error.message, ...error.status !== void 0 ? { status: error.status } : {}, details: { isApiError: error.isApiError, isNetworkError: error.isNetworkError, isAuthError: error.isAuthError, isClientError: error.isClientError, isServerError: error.isServerError, originalError: error.originalError } }, stack: error.stack, context: httpContext }; return logEntry2; } if (error instanceof KrapiError) { const httpErrorDetails = error.details?.httpError; const enhancedContext = { ...error.requestId ? { requestId: error.requestId } : {}, ...context, // Extract HTTP context from error details ...httpErrorDetails ? { httpMethod: httpErrorDetails.method, httpUrl: httpErrorDetails.url, httpStatus: httpErrorDetails.status, requestBody: httpErrorDetails.requestBody, requestQuery: httpErrorDetails.requestQuery, requestHeaders: httpErrorDetails.requestHeaders, responseData: httpErrorDetails.responseData, responseHeaders: httpErrorDetails.responseHeaders } : {}, // Include all error details ...error.details ? { errorDetails: error.details } : {} }; const logEntry2 = { timestamp, level: getLogLevelFromError(error), message: error.message, error: { code: error.code, message: error.message, ...error.status !== void 0 ? { status: error.status } : {}, ...error.details ? { details: error.details } : {} }, stack: error.stack, context: enhancedContext }; return logEntry2; } if (error instanceof Error) { const logEntry2 = { timestamp, level: "error", message: error.message, error: { code: "UNKNOWN_ERROR", message: error.message, details: { errorName: error.name, originalError: error } }, stack: error.stack, ...context ? { context } : {} }; return logEntry2; } const errorString = String(error); const logEntry = { timestamp, level: "error", message: errorString, error: { code: "UNKNOWN_ERROR", message: errorString, details: { errorType: typeof error, errorValue: error } }, ...context ? { context } : {} }; return logEntry; } function formatErrorMessage(entry) { const parts = [ `[${entry.error.code}]`, entry.message ]; if (entry.error.status) { parts.push(`(HTTP ${entry.error.status})`); } if (entry.context?.operation) { parts.push(`[${entry.context.operation}]`); } if (entry.context?.requestId) { parts.push(`[Request: ${entry.context.requestId}]`); } return parts.join(" "); } function getLogLevelFromError(error) { if (error.status && error.status >= 500) { return "error"; } if (error.status && error.status >= 400 && error.status < 500) { if (error.code === "VALIDATION_ERROR" || error.code === "BAD_REQUEST") { return "warn"; } return "error"; } if (error.code === "UNAUTHORIZED" || error.code === "FORBIDDEN") { return "warn"; } if (error.code === "NOT_FOUND") { return "info"; } return "error"; } function logServiceOperationError(logger, error, service, operation, inputData, additionalContext) { const enhancedContext = { service, operation, inputData: inputData || {}, ...additionalContext || {} }; logError(logger, error, enhancedContext); } var init_error_logger = __esm({ "src/utils/error-logger.ts"() { "use strict"; init_krapi_error(); init_http_error(); } }); // src/admin-service.ts var admin_service_exports = {}; __export(admin_service_exports, { AdminService: () => AdminService }); var import_crypto7, AdminService; var init_admin_service = __esm({ "src/admin-service.ts"() { "use strict"; import_crypto7 = __toESM(require("crypto")); init_krapi_error(); init_error_handler(); init_error_logger(); AdminService = class { /** * Create a new AdminService instance * * @param {DatabaseConnection} databaseConnection - Database connection * @param {Logger} logger - Logger instance */ constructor(databaseConnection, logger) { this.db = databaseConnection; this.logger = logger; } /** * Set backup service (called from BackendSDK constructor) * * @param {BackupService} backupService - Backup service instance * @returns {void} */ setBackupService(backupService) { this._backupService = backupService; } /** * Get all admin users * * @param {Object} [options] - Query options * @param {number} [options.limit] - Maximum number of users * @param {number} [options.offset] - Number of users to skip * @param {string} [options.search] - Search term for username/email * @param {boolean} [options.active] - Filter by active status * @returns {Promise<AdminUser[]>} Array of admin users * @throws {Error} If query fails * * @example * const users = await adminService.getUsers({ limit: 10, active: true }); */ async getUsers(options) { try { let query = "SELECT * FROM admin_users WHERE 1=1"; const params = []; let paramCount = 0; if (options?.active !== void 0) { paramCount++; query += ` AND is_active = $${paramCount}`; params.push(options.active); } if (options?.search) { paramCount++; query += ` AND (username LIKE $${paramCount} OR email LIKE $${paramCount})`; params.push(`%${options.search}%`); } query += " ORDER BY created_at DESC"; if (options?.limit) { paramCount++; query += ` LIMIT $${paramCount}`; params.push(options.limit); } if (options?.offset) { paramCount++; query += ` OFFSET $${paramCount}`; params.push(options.offset); } const result = await this.db.query(query, params); return result.rows; } catch (error) { logServiceOperationError( this.logger, error, "AdminService", "getUsers", { options }, {} ); throw normalizeError(error, "INTERNAL_ERROR", { operation: "getUsers", options }); } } /** * Get admin user by ID * * @param {string} userId - User ID * @returns {Promise<AdminUser | null>} Admin user or null if not found * @throws {Error} If query fails * * @example * const user = await adminService.getUserById('user-id'); */ async getUserById(userId) { try { const result = await this.db.query( "SELECT * FROM admin_users WHERE id = $1", [userId] ); return result.rows.length > 0 ? result.rows[0] : null; } catch (error) { logServiceOperationError( this.logger, error, "AdminService", "getUserById", { userId }, {} ); throw normalizeError(error, "INTERNAL_ERROR", { operation: "getUserById", userId }); } } /** * Create a new admin user * * Creates a new admin user account with the provided data. * * @param {Omit<AdminUser, "id" | "created_at" | "updated_at">} userData - User creation data * @param {string} userData.username - Username (required) * @param {string} userData.email - Email address (required) * @param {string} userData.password_hash - Hashed password (required) * @param {string} userData.role - User role * @param {string} userData.access_level - Access level * @param {string[]} userData.permissions - Permission scopes * @param {boolean} userData.active - Whether user is active * @param {string} [userData.api_key] - Optional API key * @returns {Promise<AdminUser>} Created admin user * @throws {Error} If creation fails or user already exists * * @example * const user = await adminService.createUser({ * username: 'newadmin', * email: 'admin@example.com', * password_hash: 'hashed_password', * role: 'admin', * access_level: 'full', * permissions: ['admin:read', 'admin:write'], * active: true * }); */ async createUser(userData) { try { const userId = import_crypto7.default.randomUUID(); const now = (/* @__PURE__ */ new Date()).toISOString(); await this.db.query( `INSERT INTO admin_users (id, username, email, password_hash, role, access_level, permissions, is_active, api_key, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, [ userId, userData.username, userData.email, userData.password_hash, userData.role, userData.access_level, JSON.stringify(userData.permissions), // SQLite stores arrays as JSON strings userData.active ? 1 : 0, // SQLite uses INTEGER 1/0 for booleans userData.api_key, now, now ] ); const result = await this.db.query( "SELECT * FROM admin_users WHERE id = $1", [userId] ); return result.rows[0]; } catch (error) { const inputData = { username: userData.username, email: userData.email, role: userData.role, access_level: userData.access_level, permissions: userData.permissions // Note: password is intentionally excluded for security }; logServiceOperationError( this.logger, error, "AdminService", "createUser", inputData, {} ); throw normalizeError(error, "INTERNAL_ERROR", { operation: "createUser", username: userData.username, email: userData.email }); } } /** * Update an admin user * * Updates admin user information with the provided data. * * @param {string} userId - User ID * @param {Partial<AdminUser>} updates - User update data * @returns {Promise<AdminUser | null>} Updated user or null if not found * @throws {Error} If update fails * * @example * const updated = await adminService.updateUser('user-id', { * role: 'master_admin', * permissions: ['master'] * }); */ async updateUser(userId, updates) { try { const fields = []; const values = []; let paramCount = 1; if (updates.username !== void 0) { fields.push(`username = $${paramCount++}`); values.push(updates.username); } if (updates.email !== void 0) { fields.push(`email = $${paramCount++}`); values.push(updates.email); } if (updates.role !== void 0) { fields.push(`role = $${paramCount++}`); values.push(updates.role); } if (updates.access_level !== void 0) { fields.push(`access_level = $${paramCount++}`); values.push(updates.access_level); } if (updates.permissions !== void 0) { fields.push(`permissions = $${paramCount++}`); values.push(updates.permissions); } if (updates.active !== void 0) { fields.push(`is_active = $${paramCount++}`); values.push(updates.active); } if (updates.api_key !== void 0) { fields.push(`api_key = $${paramCount++}`); values.push(updates.api_key); } if (fields.length === 0) { return this.getUserById(userId); } fields.push(`updated_at = $${paramCount++}`); values.push((/* @__PURE__ */ new Date()).toISOString()); values.push(userId); await this.db.query( `UPDATE admin_users SET ${fields.join( ", " )} WHERE id = $${paramCount}`, values ); const result = await this.db.query( "SELECT * FROM admin_users WHERE id = $1", [userId] ); return result.rows.length > 0 ? result.rows[0] : null; } catch (error) { this.logger.error("Failed to update admin user:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "updateUser", userId }); } } /** * Delete an admin user * * Permanently deletes an admin user from the database. * * @param {string} userId - User ID * @returns {Promise<boolean>} True if deletion successful * @throws {Error} If deletion fails * * @example * const deleted = await adminService.deleteUser('user-id'); */ async deleteUser(userId) { try { const result = await this.db.query( "DELETE FROM admin_users WHERE id = $1", [userId] ); return result.rowCount > 0; } catch (error) { this.logger.error("Failed to delete admin user:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "deleteUser", userId }); } } /** * Toggle user active status * * Toggles a user's active/inactive status. * * @param {string} userId - User ID * @returns {Promise<AdminUser | null>} Updated user or null if not found * @throws {Error} If toggle fails * * @example * const user = await adminService.toggleUserStatus('user-id'); */ async toggleUserStatus(userId) { try { const now = (/* @__PURE__ */ new Date()).toISOString(); await this.db.query( `UPDATE admin_users SET is_active = CASE WHEN is_active = 1 THEN 0 ELSE 1 END, updated_at = $1 WHERE id = $2`, [now, userId] ); const result = await this.db.query( "SELECT * FROM admin_users WHERE id = $1", [userId] ); return result.rows.length > 0 ? result.rows[0] : null; } catch (error) { this.logger.error("Failed to toggle user status:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "toggleUserStatus", userId }); } } /** * Get all API keys for a user * * Retrieves all API keys owned by a specific admin user. * * @param {string} userId - User ID * @returns {Promise<ApiKey[]>} Array of API keys * @throws {Error} If query fails * * @example * const apiKeys = await adminService.getUserApiKeys('user-id'); */ async getUserApiKeys(userId) { try { const result = await this.db.query( "SELECT * FROM api_keys WHERE owner_id = $1 AND type = 'admin' ORDER BY created_at DESC", [userId] ); return result.rows; } catch (error) { this.logger.error("Failed to get user API keys:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "getUserApiKeys", userId }); } } /** * Create an API key for a user * * Creates a new admin API key for a specific user. * * @param {string} userId - User ID * @param {Object} apiKeyData - API key data * @param {string} apiKeyData.name - API key name * @param {string} apiKeyData.key - API key value * @param {string[]} apiKeyData.scopes - Permission scopes * @param {string[]} [apiKeyData.project_ids] - Project IDs (for project-scoped keys) * @param {string} [apiKeyData.expires_at] - Expiration date * @returns {Promise<ApiKey>} Created API key * @throws {Error} If creation fails * * @example * const apiKey = await adminService.createUserApiKey('user-id', { * name: 'My API Key', * key: 'ak_...', * scopes: ['admin:read', 'admin:write'] * }); */ async createUserApiKey(userId, apiKeyData) { try { const keyId = import_crypto7.default.randomUUID(); const now = (/* @__PURE__ */ new Date()).toISOString(); await this.db.query( `INSERT INTO api_keys (id, key, name, type, owner_id, scopes, project_ids, expires_at, created_at, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [ keyId, apiKeyData.key, apiKeyData.name, "admin", userId, JSON.stringify(apiKeyData.scopes), // SQLite stores arrays as JSON strings JSON.stringify(apiKeyData.project_ids || []), // SQLite stores arrays as JSON strings apiKeyData.expires_at, now, 1 // is_active = true ] ); const result = await this.db.query( "SELECT * FROM api_keys WHERE id = $1", [keyId] ); return result.rows[0]; } catch (error) { this.logger.error("Failed to create user API key:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "createUserApiKey", userId }); } } /** * Create an API key (with auto-generation) * * Creates a new admin API key with auto-generated key value. * * @param {string} userId - User ID * @param {Object} keyData - API key data * @param {string} keyData.name - API key name * @param {string[]} keyData.permissions - Permission scopes * @param {string} [keyData.expires_at] - Expiration date * @returns {Promise<{key: string, data: ApiKey}>} Generated key value and API key data * @throws {Error} If creation fails * * @example * const { key, data } = await adminService.createApiKey('user-id', { * name: 'My API Key', * permissions: ['admin:read'] * }); * console.log(`API Key: ${key}`); // Save this securely! */ async createApiKey(userId, keyData) { try { const apiKey = `ak_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`; const keyId = import_crypto7.default.randomUUID(); const now = (/* @__PURE__ */ new Date()).toISOString(); await this.db.query( `INSERT INTO api_keys (id, key, name, type, owner_id, scopes, expires_at, created_at, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [ keyId, apiKey, keyData.name, "admin", userId, JSON.stringify(keyData.permissions), // SQLite stores arrays as JSON strings keyData.expires_at, now, 1 // is_active = true ] ); const result = await this.db.query( "SELECT * FROM api_keys WHERE id = $1", [keyId] ); const createdKey = result.rows[0]; return { key: apiKey, data: createdKey }; } catch (error) { this.logger.error("Failed to create API key:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "createApiKey" }); } } /** * Create a master API key * * Creates a master API key with full system access. * * @returns {Promise<ApiKey>} Created master API key * @throws {Error} If creation fails * * @example * const masterKey = await adminService.createMasterApiKey(); * console.log(`Master API Key: ${masterKey.key}`); // Save this securely! */ async createMasterApiKey() { try { const masterKey = `mak_${Math.random().toString(36).substring(2, 15)}`; const keyId = import_crypto7.default.randomUUID(); const now = (/* @__PURE__ */ new Date()).toISOString(); await this.db.query( `INSERT INTO api_keys (id, key, name, type, owner_id, scopes, created_at, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [ keyId, masterKey, "Master API Key", "master", "system", // System owner for master key JSON.stringify(["master"]), // SQLite stores arrays as JSON strings now, 1 // is_active = true ] ); const result = await this.db.query( "SELECT * FROM api_keys WHERE id = $1", [keyId] ); return result.rows[0]; } catch (error) { this.logger.error("Failed to create master API key:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "createMasterApiKey" }); } } /** * Delete an API key * * Soft deletes an API key by marking it as inactive. * * @param {string} keyId - API key ID * @returns {Promise<boolean>} True if deletion successful * @throws {Error} If deletion fails * * @example * const deleted = await adminService.deleteApiKey('key-id'); */ async deleteApiKey(keyId) { try { const result = await this.db.query( "UPDATE api_keys SET is_active = false WHERE id = $1", [keyId] ); return result.rowCount > 0; } catch (error) { this.logger.error("Failed to delete API key:", error); throw normalizeError(error, "INTERNAL_ERROR", { operation: "deleteApiKey" }); } } /** * Get all API keys for a project * * Retrieves all API keys associated with a project. * * @param {string} projectId - Project ID * @returns {Promise<ApiKey[]>} Array of project API keys * @throws {Error} If query fails * * @example * const apiKeys = await adminService.getProjectApiKeys('project-id'); */ async getProjectApiKeys(projectId) { try { const result = await this.db.query( `SELECT * FROM api_keys WHERE ( project_ids LIKE $1 OR project_ids LIKE $2 OR project_ids LIKE $3 OR JSON_EXTRACT(project_ids, '$') IS NOT NULL AND EXISTS ( SELECT 1 FROM json_each(project_ids) WHERE json_each.value = $4 ) ) AND is_active = true ORDER BY created_at DESC`, [ `%"${projectId}"%`, `%"${projectId}",%`, `%,"${projectId}"%`, projectId ] ); if (result.rows.length === 0) { const fallbackResult = await this.db.query( `SELECT * FROM api_keys WHERE project_ids LIKE $1 AND is_active = true ORDER BY created_at DESC`, [`%${projectId}%`] ); return fallbackResult.rows; } return result.rows; } catch { try { const fallbackResult = await this.db.query( `SELECT * FROM api_keys WHERE project_ids LIKE $1 AND is_active = true ORDER BY created_at DESC`, [`%${projectId}%`] ); return fallbackResult.rows; } catch (fallbackError) { this.logger.error("Failed to get project API keys:", fallbackError); throw normalizeError(fallbackError, "INTERNAL_ERROR", { operation: "getProjectApiKeys" }); } } } /** * Create a project API key * * Creates a new API key for a project with auto-generated key value. * * @param {string} projectId - Project ID * @param {Object} keyData - API key data * @param {string} keyData.name - API key name * @param {string} [keyData.description] - API key description * @param {string[]} keyData.scopes - Permission scopes * @param {string} [keyData.expires_at] - Expiration date * @pa