Files
claude-task-master/scripts/modules/user-management.js
Eyal Toledano 4e9d58a1b0 feat(config): Restructure .taskmasterconfig and enhance gateway integration
Config Structure Changes and Gateway Integration

## Configuration Structure Changes
- Restructured .taskmasterconfig to use 'account' section for user settings
- Moved userId, userEmail, mode, telemetryEnabled from global to account section
- API keys remain isolated in .env file (not accessible to AI)
- Enhanced getUserId() to always return value, never null (sets default '1234567890')

## Gateway Integration Enhancements
- Updated registerUserWithGateway() to accept both email and userId parameters
- Enhanced /auth/init endpoint integration for existing user validation
- API key updates automatically written to .env during registration process
- Improved user identification and validation flow

## Code Updates for New Structure
- Fixed config-manager.js getter functions for account section access
- Updated user-management.js to use config.account.userId/mode
- Modified telemetry-submission.js to read from account section
- Added getTelemetryEnabled() function with proper account section access
- Enhanced telemetry configuration reading with new structure

## Comprehensive Test Updates
- Updated integration tests (init-config.test.js) for new config structure
- Fixed unit tests (config-manager.test.js) with updated default config
- Updated telemetry tests (telemetry-submission.test.js) for account structure
- Added missing getTelemetryEnabled mock to ai-services-unified.test.js
- Fixed all test expectations to use config.account.* instead of config.global.*
- Removed references to deprecated config.subscription object

## Configuration Access Consistency
- Standardized configuration access patterns across entire codebase
- Clean separation: user settings in account, API keys in .env, models/global in respective sections
- All tests passing with new configuration structure
- Maintained backward compatibility during transition

Changes support enhanced telemetry system with proper user management and gateway integration while maintaining security through API key isolation.
2025-05-30 18:53:16 -04:00

316 lines
8.4 KiB
JavaScript

import fs from "fs";
import path from "path";
import { log, findProjectRoot } from "./utils.js";
import { getConfig, writeConfig } from "./config-manager.js";
/**
* Registers or finds a user via the gateway's /auth/init endpoint
* @param {string|null} email - Optional user's email address (only needed for billing)
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {Promise<{success: boolean, userId: string, token: string, isNewUser: boolean, error?: string}>}
*/
async function registerUserWithGateway(email = null, explicitRoot = null) {
try {
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// Email is optional - only send if provided
const requestBody = email ? { email } : {};
const response = await fetch(`${gatewayUrl}/auth/init`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
userId: "",
token: "",
isNewUser: false,
error: `Gateway registration failed: ${response.status} ${errorText}`,
};
}
const result = await response.json();
if (result.success && result.data) {
return {
success: true,
userId: result.data.userId,
token: result.data.token,
isNewUser: result.data.isNewUser,
};
} else {
return {
success: false,
userId: "",
token: "",
isNewUser: false,
error: "Invalid response format from gateway",
};
}
} catch (error) {
return {
success: false,
userId: "",
token: "",
isNewUser: false,
error: `Network error: ${error.message}`,
};
}
}
/**
* Updates the user configuration with gateway registration results
* @param {string} userId - User ID from gateway
* @param {string} token - API token from gateway
* @param {string} mode - User mode ('byok' or 'hosted')
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} Success status
*/
function updateUserConfig(userId, token, mode, explicitRoot = null) {
try {
const config = getConfig(explicitRoot);
// Ensure account section exists
if (!config.account) {
config.account = {};
}
// Update user configuration in account section
config.account.userId = userId;
config.account.mode = mode; // 'byok' or 'hosted'
// Write API token to .env file (not config)
if (token) {
writeApiKeyToEnv(token, explicitRoot);
}
// Save updated config
const success = writeConfig(config, explicitRoot);
if (success) {
log("info", `User configuration updated: userId=${userId}, mode=${mode}`);
} else {
log("error", "Failed to write updated user configuration");
}
return success;
} catch (error) {
log("error", `Error updating user config: ${error.message}`);
return false;
}
}
/**
* Writes the API token to the .env file
* @param {string} token - API token to write
* @param {string|null} explicitRoot - Optional explicit project root path
*/
function writeApiKeyToEnv(token, explicitRoot = null) {
try {
// Determine project root
let rootPath = explicitRoot;
if (!rootPath) {
rootPath = findProjectRoot();
if (!rootPath) {
log("warn", "Could not determine project root for .env file");
return;
}
}
const envPath = path.join(rootPath, ".env");
let envContent = "";
// Read existing .env content if file exists
if (fs.existsSync(envPath)) {
envContent = fs.readFileSync(envPath, "utf8");
}
// Check if TASKMASTER_API_KEY already exists
const lines = envContent.split("\n");
let keyExists = false;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith("TASKMASTER_API_KEY=")) {
lines[i] = `TASKMASTER_API_KEY=${token}`;
keyExists = true;
break;
}
}
// Add key if it doesn't exist
if (!keyExists) {
if (envContent && !envContent.endsWith("\n")) {
envContent += "\n";
}
envContent += `TASKMASTER_API_KEY=${token}\n`;
} else {
envContent = lines.join("\n");
}
// Write updated content
fs.writeFileSync(envPath, envContent);
log("info", "API key written to .env file");
} catch (error) {
log("error", `Failed to write API key to .env: ${error.message}`);
}
}
/**
* Gets the current user mode from configuration
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {string} User mode ('byok', 'hosted', or 'unknown')
*/
function getUserMode(explicitRoot = null) {
try {
const config = getConfig(explicitRoot);
return config?.account?.mode || "unknown";
} catch (error) {
log("error", `Error getting user mode: ${error.message}`);
return "unknown";
}
}
/**
* Checks if user is in hosted mode
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} True if user is in hosted mode
*/
function isHostedMode(explicitRoot = null) {
return getUserMode(explicitRoot) === "hosted";
}
/**
* Checks if user is in BYOK mode
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} True if user is in BYOK mode
*/
function isByokMode(explicitRoot = null) {
return getUserMode(explicitRoot) === "byok";
}
/**
* Complete user setup: register with gateway and configure TaskMaster
* @param {string|null} email - Optional user's email (only needed for billing)
* @param {string} mode - User's mode: 'byok' or 'hosted'
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {Promise<{success: boolean, userId: string, mode: string, error?: string}>}
*/
async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
try {
// Step 1: Register with gateway (email optional)
const registrationResult = await registerUserWithGateway(
email,
explicitRoot
);
if (!registrationResult.success) {
return {
success: false,
userId: "",
mode: "",
error: registrationResult.error,
};
}
// Step 2: Update config with userId and mode
const configResult = await updateUserConfig(
registrationResult.userId,
mode,
explicitRoot
);
if (!configResult) {
return {
success: false,
userId: registrationResult.userId,
mode: "",
error: "Failed to update user configuration",
};
}
return {
success: true,
userId: registrationResult.userId,
mode: mode,
message: email
? `User setup complete with email ${email}`
: "User setup complete (email will be collected during billing setup)",
};
} catch (error) {
return {
success: false,
userId: "",
mode: "",
error: `Setup failed: ${error.message}`,
};
}
}
/**
* Initialize TaskMaster user (typically called during init)
* Gets userId from gateway without requiring email upfront
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {Promise<{success: boolean, userId: string, error?: string}>}
*/
async function initializeUser(explicitRoot = null) {
try {
// Register with gateway without email
const result = await registerUserWithGateway(null, explicitRoot);
if (!result.success) {
return {
success: false,
userId: "",
error: result.error,
};
}
// Update config with userId, token, and default hosted mode
const configResult = updateUserConfig(
result.userId,
result.token, // Include the token parameter
"hosted", // Default to hosted mode until user chooses plan
explicitRoot
);
if (!configResult) {
return {
success: false,
userId: result.userId,
error: "Failed to update user configuration",
};
}
return {
success: true,
userId: result.userId,
message: result.isNewUser
? "New user registered with gateway"
: "Existing user found in gateway",
};
} catch (error) {
return {
success: false,
userId: "",
error: `Initialization failed: ${error.message}`,
};
}
}
export {
registerUserWithGateway,
updateUserConfig,
writeApiKeyToEnv,
getUserMode,
isHostedMode,
isByokMode,
setupUser,
initializeUser,
};