feat: integrate Supabase authenticated users
- Updated init.js, ai-services-unified.js, user-management.js, telemetry-submission.js, and .taskmasterconfig to support Supabase authentication flow and authenticated gateway calls
This commit is contained in:
@@ -30,8 +30,8 @@
|
|||||||
"ollamaBaseUrl": "http://localhost:11434/api"
|
"ollamaBaseUrl": "http://localhost:11434/api"
|
||||||
},
|
},
|
||||||
"account": {
|
"account": {
|
||||||
"userId": "ee196145-8f01-41b9-a9ce-85faae397254",
|
"userId": "1234567890",
|
||||||
"email": "eyal@testing.com",
|
"email": "",
|
||||||
"mode": "hosted",
|
"mode": "hosted",
|
||||||
"telemetryEnabled": true
|
"telemetryEnabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import { execSync } from "child_process";
|
|||||||
import {
|
import {
|
||||||
initializeUser,
|
initializeUser,
|
||||||
registerUserWithGateway,
|
registerUserWithGateway,
|
||||||
|
initializeBYOKUser,
|
||||||
|
initializeHostedUser,
|
||||||
} from "./modules/user-management.js";
|
} from "./modules/user-management.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|||||||
@@ -284,6 +284,10 @@ async function _attemptProviderCallWithRetries(
|
|||||||
* @param {string} initialRole - The initial client role
|
* @param {string} initialRole - The initial client role
|
||||||
* @returns {Promise<object>} AI response with usage data
|
* @returns {Promise<object>} AI response with usage data
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Calls the TaskMaster gateway for AI processing (hosted mode only).
|
||||||
|
* BYOK users don't use this function - they make direct API calls.
|
||||||
|
*/
|
||||||
async function _callGatewayAI(
|
async function _callGatewayAI(
|
||||||
serviceType,
|
serviceType,
|
||||||
callParams,
|
callParams,
|
||||||
@@ -301,9 +305,17 @@ async function _callGatewayAI(
|
|||||||
|
|
||||||
// Get user auth info for headers
|
// Get user auth info for headers
|
||||||
const userMgmt = await import("./user-management.js");
|
const userMgmt = await import("./user-management.js");
|
||||||
|
const config = getConfig(projectRoot);
|
||||||
|
const mode = config.account?.mode || "byok";
|
||||||
|
|
||||||
|
// Both BYOK and hosted users have the same user token
|
||||||
|
// BYOK users just don't use it for AI calls (they use their own API keys)
|
||||||
const userToken = await userMgmt.getUserToken(projectRoot);
|
const userToken = await userMgmt.getUserToken(projectRoot);
|
||||||
const userEmail = await userMgmt.getUserEmail(projectRoot);
|
const userEmail = await userMgmt.getUserEmail(projectRoot);
|
||||||
|
|
||||||
|
// Note: BYOK users will have both token and email, but won't use this function
|
||||||
|
// since they make direct API calls with their own keys
|
||||||
|
|
||||||
if (!userToken) {
|
if (!userToken) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"User token not found. Run 'task-master init' to register with gateway."
|
"User token not found. Run 'task-master init' to register with gateway."
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export async function submitTelemetryData(telemetryData) {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Service-ID": telemetryConfig.serviceId, // Hardcoded service ID
|
"x-taskmaster-service-id": telemetryConfig.serviceId, // Hardcoded service ID
|
||||||
Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token
|
Authorization: `Bearer ${telemetryConfig.apiKey}`, // User's Bearer token
|
||||||
"X-User-Email": telemetryConfig.email, // User's email from config
|
"X-User-Email": telemetryConfig.email, // User's email from config
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -293,65 +293,170 @@ async function setupUser(email = null, mode = "hosted", explicitRoot = null) {
|
|||||||
* @returns {Promise<{success: boolean, userId: string, error?: string}>}
|
* @returns {Promise<{success: boolean, userId: string, error?: string}>}
|
||||||
*/
|
*/
|
||||||
async function initializeUser(explicitRoot = null) {
|
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 {
|
try {
|
||||||
// Try to register with gateway without email
|
const gatewayUrl =
|
||||||
const result = await registerUserWithGateway(null, explicitRoot);
|
process.env.TASKMASTER_GATEWAY_URL || "http://localhost:4444";
|
||||||
|
|
||||||
// If gateway call succeeded, use the returned values
|
// Check if we already have an anonymous user ID stored
|
||||||
if (result.success) {
|
let config = getConfig(projectRoot);
|
||||||
// Update config with userId, token, and preserve existing mode (or default)
|
const existingAnonymousUserId = config?.account?.userId;
|
||||||
const existingMode = getUserMode(explicitRoot);
|
|
||||||
const modeToUse = existingMode !== "unknown" ? existingMode : "byok";
|
|
||||||
|
|
||||||
const configResult = updateUserConfig(
|
// Prepare headers for the request
|
||||||
result.userId,
|
const headers = {
|
||||||
result.token,
|
"Content-Type": "application/json",
|
||||||
modeToUse,
|
"X-TaskMaster-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
|
||||||
null,
|
};
|
||||||
explicitRoot
|
|
||||||
|
// If we have an existing anonymous user ID, try to reuse it
|
||||||
|
if (existingAnonymousUserId && existingAnonymousUserId !== "1234567890") {
|
||||||
|
headers["X-Anonymous-User-ID"] = existingAnonymousUserId;
|
||||||
|
log(
|
||||||
|
"info",
|
||||||
|
`Attempting to reuse existing anonymous user: ${existingAnonymousUserId}`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!configResult) {
|
// Call gateway /auth/anonymous to create or reuse a user account
|
||||||
return {
|
// BYOK users still get an account for potential future hosted mode switch
|
||||||
success: false,
|
const response = await fetch(`${gatewayUrl}/auth/anonymous`, {
|
||||||
userId: result.userId,
|
method: "POST",
|
||||||
error: "Failed to update user configuration",
|
headers,
|
||||||
};
|
body: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// Log whether user was reused or newly created
|
||||||
|
if (result.isReused) {
|
||||||
|
log(
|
||||||
|
"info",
|
||||||
|
`Successfully reused existing anonymous user: ${result.anonymousUserId}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log("info", `Created new anonymous user: ${result.anonymousUserId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.userEmail =
|
||||||
|
result.user.email ||
|
||||||
|
`anon-${result.anonymousUserId || result.user.id}@taskmaster.temp`;
|
||||||
|
config.account.telemetryEnabled = false; // BYOK users don't send telemetry by default
|
||||||
|
|
||||||
|
writeConfig(config, projectRoot);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
userId: result.userId,
|
userId: result.anonymousUserId || result.user.id,
|
||||||
message: result.isNewUser
|
token: result.session?.access_token || null,
|
||||||
? "New user registered with gateway"
|
mode: "byok",
|
||||||
: "Existing user found in gateway",
|
isAnonymous: true,
|
||||||
|
isReused: result.isReused || false,
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
|
const errorText = await response.text();
|
||||||
// Gateway call failed - check if we have existing credentials to use
|
|
||||||
const existingUserId = getUserId(explicitRoot);
|
|
||||||
const existingToken = getUserToken(explicitRoot);
|
|
||||||
|
|
||||||
if (existingUserId && existingUserId !== "1234567890" && existingToken) {
|
|
||||||
// We have existing credentials, use them (gateway unavailable scenario)
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
userId: existingUserId,
|
error: `Gateway not available: ${response.status} ${errorText}`,
|
||||||
message: "Gateway unavailable, using existing user credentials",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// No existing credentials and gateway failed
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
userId: "",
|
|
||||||
error: result.error,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
userId: "",
|
error: `Network error: ${error.message}`,
|
||||||
error: `Initialization failed: ${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}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,6 +524,8 @@ export {
|
|||||||
isByokMode,
|
isByokMode,
|
||||||
setupUser,
|
setupUser,
|
||||||
initializeUser,
|
initializeUser,
|
||||||
|
initializeBYOKUser,
|
||||||
|
initializeHostedUser,
|
||||||
getUserToken,
|
getUserToken,
|
||||||
getUserEmail,
|
getUserEmail,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ describe("Telemetry Submission Service", () => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Service-ID": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
|
"x-taskmaster-service-id": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
|
||||||
Authorization: "Bearer test-api-key",
|
Authorization: "Bearer test-api-key",
|
||||||
"X-User-Email": "test@example.com",
|
"X-User-Email": "test@example.com",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user