feat: add oauth with remote server (#1178)
This commit is contained in:
163
packages/tm-core/src/auth/auth-manager.ts
Normal file
163
packages/tm-core/src/auth/auth-manager.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Authentication manager for Task Master CLI
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthCredentials,
|
||||
OAuthFlowOptions,
|
||||
AuthenticationError,
|
||||
AuthConfig
|
||||
} from './types';
|
||||
import { CredentialStore } from './credential-store';
|
||||
import { OAuthService } from './oauth-service';
|
||||
import { SupabaseAuthClient } from '../clients/supabase-client';
|
||||
|
||||
/**
|
||||
* Authentication manager class
|
||||
*/
|
||||
export class AuthManager {
|
||||
private static instance: AuthManager;
|
||||
private credentialStore: CredentialStore;
|
||||
private oauthService: OAuthService;
|
||||
private supabaseClient: SupabaseAuthClient;
|
||||
|
||||
private constructor(config?: Partial<AuthConfig>) {
|
||||
this.credentialStore = new CredentialStore(config);
|
||||
this.supabaseClient = new SupabaseAuthClient();
|
||||
this.oauthService = new OAuthService(this.credentialStore, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
static getInstance(config?: Partial<AuthConfig>): AuthManager {
|
||||
if (!AuthManager.instance) {
|
||||
AuthManager.instance = new AuthManager(config);
|
||||
}
|
||||
return AuthManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored authentication credentials
|
||||
*/
|
||||
getCredentials(): AuthCredentials | null {
|
||||
return this.credentialStore.getCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth 2.0 Authorization Code Flow with browser handling
|
||||
*/
|
||||
async authenticateWithOAuth(
|
||||
options: OAuthFlowOptions = {}
|
||||
): Promise<AuthCredentials> {
|
||||
return this.oauthService.authenticate(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authorization URL (for browser opening)
|
||||
*/
|
||||
getAuthorizationUrl(): string | null {
|
||||
return this.oauthService.getAuthorizationUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with API key
|
||||
* Note: This would require a custom implementation or Supabase RLS policies
|
||||
*/
|
||||
async authenticateWithApiKey(apiKey: string): Promise<AuthCredentials> {
|
||||
const token = apiKey.trim();
|
||||
if (!token || token.length < 10) {
|
||||
throw new AuthenticationError('Invalid API key', 'INVALID_API_KEY');
|
||||
}
|
||||
|
||||
const authData: AuthCredentials = {
|
||||
token,
|
||||
tokenType: 'api_key',
|
||||
userId: 'api-user',
|
||||
email: undefined,
|
||||
expiresAt: undefined, // API keys don't expire
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(authData);
|
||||
return authData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh authentication token
|
||||
*/
|
||||
async refreshToken(): Promise<AuthCredentials> {
|
||||
const authData = this.credentialStore.getCredentials({
|
||||
allowExpired: true
|
||||
});
|
||||
|
||||
if (!authData || !authData.refreshToken) {
|
||||
throw new AuthenticationError(
|
||||
'No refresh token available',
|
||||
'NO_REFRESH_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Supabase client to refresh the token
|
||||
const response = await this.supabaseClient.refreshSession(
|
||||
authData.refreshToken
|
||||
);
|
||||
|
||||
// Update authentication data
|
||||
const newAuthData: AuthCredentials = {
|
||||
...authData,
|
||||
token: response.token,
|
||||
refreshToken: response.refreshToken,
|
||||
expiresAt: response.expiresAt,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.credentialStore.saveCredentials(newAuthData);
|
||||
return newAuthData;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and clear credentials
|
||||
*/
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
// First try to sign out from Supabase to revoke tokens
|
||||
await this.supabaseClient.signOut();
|
||||
} catch (error) {
|
||||
// Log but don't throw - we still want to clear local credentials
|
||||
console.warn('Failed to sign out from Supabase:', error);
|
||||
}
|
||||
|
||||
// Always clear local credentials (removes auth.json file)
|
||||
this.credentialStore.clearCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if authenticated
|
||||
*/
|
||||
isAuthenticated(): boolean {
|
||||
return this.credentialStore.hasValidCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization headers
|
||||
*/
|
||||
getAuthHeaders(): Record<string, string> {
|
||||
const authData = this.getCredentials();
|
||||
|
||||
if (!authData) {
|
||||
throw new AuthenticationError(
|
||||
'Not authenticated. Please authenticate first.',
|
||||
'NOT_AUTHENTICATED'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${authData.token}`
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user