feat: implement tm list remote (#1185)

This commit is contained in:
Ralph Khreish
2025-09-09 02:28:38 +02:00
parent 15900d9fd5
commit c61c73f827
32 changed files with 2757 additions and 1957 deletions

View File

@@ -6,11 +6,18 @@ import {
AuthCredentials,
OAuthFlowOptions,
AuthenticationError,
AuthConfig
AuthConfig,
UserContext
} from './types.js';
import { CredentialStore } from './credential-store.js';
import { OAuthService } from './oauth-service.js';
import { SupabaseAuthClient } from '../clients/supabase-client.js';
import {
OrganizationService,
type Organization,
type Brief,
type RemoteTask
} from '../services/organization.service.js';
import { getLogger } from '../logger/index.js';
/**
@@ -21,11 +28,28 @@ export class AuthManager {
private credentialStore: CredentialStore;
private oauthService: OAuthService;
private supabaseClient: SupabaseAuthClient;
private organizationService?: OrganizationService;
private constructor(config?: Partial<AuthConfig>) {
this.credentialStore = new CredentialStore(config);
this.supabaseClient = new SupabaseAuthClient();
this.oauthService = new OAuthService(this.credentialStore, config);
// Initialize Supabase client with session restoration
this.initializeSupabaseSession();
}
/**
* Initialize Supabase session from stored credentials
*/
private async initializeSupabaseSession(): Promise<void> {
try {
await this.supabaseClient.initialize();
} catch (error) {
// Log but don't throw - session might not exist yet
const logger = getLogger('AuthManager');
logger.debug('No existing session to restore');
}
}
/**
@@ -75,39 +99,48 @@ export class AuthManager {
}
/**
* Refresh authentication token
* Refresh authentication token using Supabase session
*/
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
);
// Use Supabase's built-in session refresh
const session = await this.supabaseClient.refreshSession();
// Update authentication data
if (!session) {
throw new AuthenticationError(
'Failed to refresh session',
'REFRESH_FAILED'
);
}
// Get existing credentials to preserve context
const existingCredentials = this.credentialStore.getCredentials({
allowExpired: true
});
// Update authentication data from session
const newAuthData: AuthCredentials = {
...authData,
token: response.token,
refreshToken: response.refreshToken,
expiresAt: response.expiresAt,
savedAt: new Date().toISOString()
token: session.access_token,
refreshToken: session.refresh_token,
userId: session.user.id,
email: session.user.email,
expiresAt: session.expires_at
? new Date(session.expires_at * 1000).toISOString()
: undefined,
savedAt: new Date().toISOString(),
selectedContext: existingCredentials?.selectedContext
};
this.credentialStore.saveCredentials(newAuthData);
return newAuthData;
} catch (error) {
throw error;
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Token refresh failed: ${(error as Error).message}`,
'REFRESH_FAILED'
);
}
}
@@ -133,4 +166,114 @@ export class AuthManager {
isAuthenticated(): boolean {
return this.credentialStore.hasValidCredentials();
}
/**
* Get the current user context (org/brief selection)
*/
getContext(): UserContext | null {
const credentials = this.getCredentials();
return credentials?.selectedContext || null;
}
/**
* Update the user context (org/brief selection)
*/
async updateContext(context: Partial<UserContext>): Promise<void> {
const credentials = this.getCredentials();
if (!credentials) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}
// Merge with existing context
const existingContext = credentials.selectedContext || {};
const newContext: UserContext = {
...existingContext,
...context,
updatedAt: new Date().toISOString()
};
// Save updated credentials with new context
const updatedCredentials: AuthCredentials = {
...credentials,
selectedContext: newContext
};
this.credentialStore.saveCredentials(updatedCredentials);
}
/**
* Clear the user context
*/
async clearContext(): Promise<void> {
const credentials = this.getCredentials();
if (!credentials) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}
// Remove context from credentials
const { selectedContext, ...credentialsWithoutContext } = credentials;
this.credentialStore.saveCredentials(credentialsWithoutContext);
}
/**
* Get the organization service instance
* Uses the Supabase client with the current session or token
*/
private async getOrganizationService(): Promise<OrganizationService> {
if (!this.organizationService) {
// First check if we have credentials with a token
const credentials = this.getCredentials();
if (!credentials || !credentials.token) {
throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
}
// Initialize session if needed (this will load from our storage adapter)
await this.supabaseClient.initialize();
// Use the SupabaseAuthClient which now has the session
const supabaseClient = this.supabaseClient.getClient();
this.organizationService = new OrganizationService(supabaseClient as any);
}
return this.organizationService;
}
/**
* Get all organizations for the authenticated user
*/
async getOrganizations(): Promise<Organization[]> {
const service = await this.getOrganizationService();
return service.getOrganizations();
}
/**
* Get all briefs for a specific organization
*/
async getBriefs(orgId: string): Promise<Brief[]> {
const service = await this.getOrganizationService();
return service.getBriefs(orgId);
}
/**
* Get a specific organization by ID
*/
async getOrganization(orgId: string): Promise<Organization | null> {
const service = await this.getOrganizationService();
return service.getOrganization(orgId);
}
/**
* Get a specific brief by ID
*/
async getBrief(briefId: string): Promise<Brief | null> {
const service = await this.getOrganizationService();
return service.getBrief(briefId);
}
/**
* Get all tasks for a specific brief
*/
async getTasks(briefId: string): Promise<RemoteTask[]> {
const service = await this.getOrganizationService();
return service.getTasks(briefId);
}
}