Files
claude-task-master/scripts/modules/user-management.js
2025-06-04 19:03:47 -04:00

517 lines
14 KiB
JavaScript

import fs from "fs";
import path from "path";
import { log, findProjectRoot } from "./utils.js";
import { getConfig, writeConfig, getUserId } 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";
// Check for existing userId and email to pass to gateway
const existingUserId = getUserId(explicitRoot);
const existingEmail = email || getUserEmail(explicitRoot);
// Build request body with existing values (gateway can handle userId for existing users)
const requestBody = {};
if (existingUserId && existingUserId !== "1234567890") {
requestBody.userId = existingUserId;
}
if (existingEmail) {
requestBody.email = existingEmail;
}
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 - User authentication token from gateway (stored in .env)
* @param {string} mode - User mode ('byok' or 'hosted')
* @param {string|null} email - Optional user email to save
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {boolean} Success status
*/
function updateUserConfig(
userId,
token,
mode,
email = null,
explicitRoot = null
) {
try {
const config = getConfig(explicitRoot);
// Ensure account section exists
if (!config.account) {
config.account = {};
}
// Ensure global section exists for email
if (!config.global) {
config.global = {};
}
// Update user configuration in account section
config.account.userId = userId;
config.account.mode = mode; // 'byok' or 'hosted'
// Save email if provided
if (email) {
config.account.email = email;
}
// Write user authentication token to .env file (not config)
if (token) {
writeApiKeyToEnv(token, explicitRoot);
}
// Save updated config
const success = writeConfig(config, explicitRoot);
if (success) {
const emailInfo = email ? `, email=${email}` : "";
log(
"info",
`User configuration updated: userId=${userId}, mode=${mode}${emailInfo}`
);
} 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 user authentication token to the .env file
* This token is used as Bearer auth for gateway API calls
* @param {string} token - Authentication 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);
} catch (error) {
log("error", `Failed to write user token 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, mode, and email
const configResult = updateUserConfig(
registrationResult.userId,
registrationResult.token,
mode,
email,
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) {
const config = getConfig(explicitRoot);
const mode = config.account?.mode || "byok";
if (mode === "byok") {
return await initializeBYOKUser(explicitRoot);
} else {
return await initializeHostedUser(explicitRoot);
}
}
async function initializeBYOKUser(projectRoot) {
try {
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// Check if we already have an anonymous user ID stored
let config = getConfig(projectRoot);
const existingAnonymousUserId = config?.account?.userId;
// Prepare headers for the request
const headers = {
"Content-Type": "application/json",
"X-TaskMaster-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
};
// If we have an existing anonymous user ID, try to reuse it
if (existingAnonymousUserId && existingAnonymousUserId !== "1234567890") {
headers["X-Anonymous-User-ID"] = existingAnonymousUserId;
}
// Call gateway /auth/anonymous to create or reuse a user account
// BYOK users still get an account for potential future hosted mode switch
const response = await fetch(`${gatewayUrl}/auth/anonymous`, {
method: "POST",
headers,
body: JSON.stringify({}),
});
if (response.ok) {
const result = await response.json();
// Store the user token (same as hosted users)
// BYOK users won't use this for AI calls, but will have it for potential mode switch
if (result.session && result.session.access_token) {
writeApiKeyToEnv(result.session.access_token, projectRoot);
}
// Update config with BYOK user info, ensuring we store the anonymous user ID
if (!config.account) {
config.account = {};
}
config.account.userId = result.anonymousUserId || result.user.id;
config.account.mode = "byok";
config.account.email =
result.user.email ||
`anon-${result.anonymousUserId || result.user.id}@taskmaster.temp`;
config.account.telemetryEnabled = true;
writeConfig(config, projectRoot);
return {
success: true,
userId: result.anonymousUserId || result.user.id,
token: result.session?.access_token || null,
mode: "byok",
isAnonymous: true,
isReused: result.isReused || false,
};
} else {
const errorText = await response.text();
return {
success: false,
error: `Gateway not available: ${response.status} ${errorText}`,
};
}
} catch (error) {
return {
success: false,
error: `Network error: ${error.message}`,
};
}
}
async function initializeHostedUser(projectRoot) {
try {
// For hosted users, we need proper authentication
// This would typically involve OAuth flow or registration
const gatewayUrl =
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
// Check if we already have stored credentials
const existingToken = getUserToken(projectRoot);
const existingUserId = getUserId(projectRoot);
if (existingToken && existingUserId && existingUserId !== "1234567890") {
// Try to validate existing credentials
try {
const response = await fetch(`${gatewayUrl}/auth/validate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${existingToken}`,
"X-TaskMaster-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
},
});
if (response.ok) {
return {
success: true,
userId: existingUserId,
token: existingToken,
mode: "hosted",
isExisting: true,
};
}
} catch (error) {
// Fall through to re-authentication
}
}
// If no valid credentials, use the existing registration flow
const registrationResult = await registerUserWithGateway(null, projectRoot);
if (registrationResult.success) {
// Update config for hosted mode
updateUserConfig(
registrationResult.userId,
registrationResult.token,
"hosted",
null,
projectRoot
);
return {
success: true,
userId: registrationResult.userId,
token: registrationResult.token,
mode: "hosted",
isNewUser: registrationResult.isNewUser,
};
} else {
return {
success: false,
error: `Hosted mode setup failed: ${registrationResult.error}`,
};
}
} catch (error) {
return {
success: false,
error: `Hosted user initialization failed: ${error.message}`,
};
}
}
/**
* Gets the current user authentication token from .env file
* This is the Bearer token used for gateway API calls
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {string|null} User authentication token or null if not found
*/
function getUserToken(explicitRoot = null) {
try {
// Determine project root
let rootPath = explicitRoot;
if (!rootPath) {
rootPath = findProjectRoot();
if (!rootPath) {
log("error", "Could not determine project root for .env file");
return null;
}
}
const envPath = path.join(rootPath, ".env");
if (!fs.existsSync(envPath)) {
return null;
}
const envContent = fs.readFileSync(envPath, "utf8");
const lines = envContent.split("\n");
for (const line of lines) {
if (line.startsWith("TASKMASTER_API_KEY=")) {
return line.substring("TASKMASTER_API_KEY=".length).trim();
}
}
return null;
} catch (error) {
log("error", `Error getting user token from .env: ${error.message}`);
return null;
}
}
/**
* Gets the current user email from configuration
* @param {string|null} explicitRoot - Optional explicit project root path
* @returns {string|null} User email or null if not found
*/
function getUserEmail(explicitRoot = null) {
try {
const config = getConfig(explicitRoot);
return config?.account?.email || null;
} catch (error) {
log("error", `Error getting user email: ${error.message}`);
return null;
}
}
export {
registerUserWithGateway,
updateUserConfig,
writeApiKeyToEnv,
getUserMode,
isHostedMode,
isByokMode,
setupUser,
initializeUser,
initializeBYOKUser,
initializeHostedUser,
getUserToken,
getUserEmail,
};