feat: implement tm list remote (#1185)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user