diff --git a/src/modules/atproto/infrastructure/publishers/ATProtoCardPublisher.ts b/src/modules/atproto/infrastructure/publishers/ATProtoCardPublisher.ts index 347d00b2..a8ccc7e2 100644 --- a/src/modules/atproto/infrastructure/publishers/ATProtoCardPublisher.ts +++ b/src/modules/atproto/infrastructure/publishers/ATProtoCardPublisher.ts @@ -8,6 +8,7 @@ import { StrongRef } from '../../domain'; import { IAgentService } from '../../application/IAgentService'; import { DID } from '../../domain/DID'; import { PublishedRecordId } from 'src/modules/cards/domain/value-objects/PublishedRecordId'; +import { AuthenticationError } from 'src/shared/core/AuthenticationError'; export class ATProtoCardPublisher implements ICardPublisher { constructor( private readonly agentService: IAgentService, @@ -45,6 +46,10 @@ export class ATProtoCardPublisher implements ICardPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( `Authentication error for ATProtoCardPublisher: ${agentResult.error.message}`, @@ -131,6 +136,10 @@ export class ATProtoCardPublisher implements ICardPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( `Authentication error for ATProtoCardPublisher: ${agentResult.error.message}`, diff --git a/src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts b/src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts index ee8131f3..eda56fcc 100644 --- a/src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts +++ b/src/modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher.ts @@ -13,6 +13,7 @@ import { CollectionLinkMapper } from '../mappers/CollectionLinkMapper'; import { StrongRef } from '../../domain'; import { IAgentService } from '../../application/IAgentService'; import { DID } from '../../domain/DID'; +import { AuthenticationError } from 'src/shared/core/AuthenticationError'; export class ATProtoCollectionPublisher implements ICollectionPublisher { constructor( @@ -43,6 +44,10 @@ export class ATProtoCollectionPublisher implements ICollectionPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( `Authentication error for ATProtoCollectionPublisher: ${agentResult.error.message}`, @@ -125,9 +130,13 @@ export class ATProtoCollectionPublisher implements ICollectionPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( - `Authenticated error for ATProtoCollectionPublisher: ${agentResult.error.message}`, + `Authentication error for ATProtoCollectionPublisher: ${agentResult.error.message}`, ), ); } @@ -225,9 +234,13 @@ export class ATProtoCollectionPublisher implements ICollectionPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( - `Authenticated error for ATProtoCollectionPublisher: ${agentResult.error.message}`, + `Authentication error for ATProtoCollectionPublisher: ${agentResult.error.message}`, ), ); } @@ -271,9 +284,13 @@ export class ATProtoCollectionPublisher implements ICollectionPublisher { await this.agentService.getAuthenticatedAgent(curatorDid); if (agentResult.isErr()) { + // Propagate authentication errors as-is + if (agentResult.error instanceof AuthenticationError) { + return err(agentResult.error); + } return err( new Error( - `Authenticated error for ATProtoCollectionPublisher: ${agentResult.error.message}`, + `Authentication error for ATProtoCollectionPublisher: ${agentResult.error.message}`, ), ); } diff --git a/src/modules/atproto/infrastructure/services/ATProtoAgentService.ts b/src/modules/atproto/infrastructure/services/ATProtoAgentService.ts index 36c44318..8ffc3396 100644 --- a/src/modules/atproto/infrastructure/services/ATProtoAgentService.ts +++ b/src/modules/atproto/infrastructure/services/ATProtoAgentService.ts @@ -5,6 +5,7 @@ import { IAgentService } from '../../application/IAgentService'; import { DID } from '../../domain/DID'; import { IAppPasswordSessionService } from '../../application/IAppPasswordSessionService'; import { ATPROTO_SERVICE_ENDPOINTS } from './ServiceEndpoints'; +import { AuthenticationError } from 'src/shared/core/AuthenticationError'; export class ATProtoAgentService implements IAgentService { constructor( @@ -27,8 +28,8 @@ export class ATProtoAgentService implements IAgentService { await this.getAuthenticatedAgentByAppPasswordSession(did); if (appPasswordAgentResult.isErr()) { return err( - new Error( - `Failed to get authenticated agent: ${oauthAgentResult.error.message} | ${appPasswordAgentResult.error.message}`, + new AuthenticationError( + `Failed to authenticate: No valid OAuth or App Password session found`, ), ); } @@ -49,12 +50,12 @@ export class ATProtoAgentService implements IAgentService { } // No session found - throw new Error('No session found for the provided DID'); + throw new AuthenticationError('No OAuth session found for the provided DID'); } catch (error) { return err( - new Error( - `Failed to get authenticated agent by OAuth session: ${error instanceof Error ? error.message : String(error)}`, - ), + error instanceof AuthenticationError + ? error + : new AuthenticationError(`OAuth authentication failed: ${error instanceof Error ? error.message : String(error)}`) ); } } @@ -67,12 +68,9 @@ export class ATProtoAgentService implements IAgentService { await this.appPasswordSessionService.getSession(did.value); if (appPasswordSessionResult.isErr()) { - return err( - new Error( - `Failed to get App Password session: ${appPasswordSessionResult.error.message}`, - ), - ); + return err(new AuthenticationError(`App Password session failed: ${appPasswordSessionResult.error.message}`)); } + const session = appPasswordSessionResult.value; if (session) { // Create an Agent with the session @@ -88,12 +86,12 @@ export class ATProtoAgentService implements IAgentService { } // No session found - throw new Error('No session found for the provided DID'); + throw new AuthenticationError('No App Password session found for the provided DID'); } catch (error) { return err( - new Error( - `Failed to get authenticated agent by App Password session: ${error instanceof Error ? error.message : String(error)}`, - ), + error instanceof AuthenticationError + ? error + : new AuthenticationError(`App Password authentication failed: ${error instanceof Error ? error.message : String(error)}`) ); } } diff --git a/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts b/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts index bf539ea1..c223941f 100644 --- a/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts +++ b/src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts @@ -17,6 +17,7 @@ import { URL } from '../../../domain/value-objects/URL'; import { CardLibraryService } from '../../../domain/services/CardLibraryService'; import { CardCollectionService } from '../../../domain/services/CardCollectionService'; import { CardContent } from '../../../domain/value-objects/CardContent'; +import { AuthenticationError } from '../../../../../shared/core/AuthenticationError'; export interface AddUrlToLibraryDTO { url: string; @@ -38,7 +39,7 @@ export class ValidationError extends UseCaseError { export class AddUrlToLibraryUseCase extends BaseUseCase< AddUrlToLibraryDTO, - Result + Result > { constructor( private cardRepository: ICardRepository, @@ -55,7 +56,7 @@ export class AddUrlToLibraryUseCase extends BaseUseCase< ): Promise< Result< AddUrlToLibraryResponseDTO, - ValidationError | AppError.UnexpectedError + ValidationError | AuthenticationError | AppError.UnexpectedError > > { try { @@ -129,6 +130,10 @@ export class AddUrlToLibraryUseCase extends BaseUseCase< const addUrlCardToLibraryResult = await this.cardLibraryService.addCardToLibrary(urlCard, curatorId); if (addUrlCardToLibraryResult.isErr()) { + // Propagate authentication errors + if (addUrlCardToLibraryResult.error instanceof AuthenticationError) { + return err(addUrlCardToLibraryResult.error); + } if ( addUrlCardToLibraryResult.error instanceof AppError.UnexpectedError ) { @@ -210,6 +215,10 @@ export class AddUrlToLibraryUseCase extends BaseUseCase< const addNoteCardToLibraryResult = await this.cardLibraryService.addCardToLibrary(noteCard, curatorId); if (addNoteCardToLibraryResult.isErr()) { + // Propagate authentication errors + if (addNoteCardToLibraryResult.error instanceof AuthenticationError) { + return err(addNoteCardToLibraryResult.error); + } if ( addNoteCardToLibraryResult.error instanceof AppError.UnexpectedError @@ -254,6 +263,10 @@ export class AddUrlToLibraryUseCase extends BaseUseCase< curatorId, ); if (addToCollectionsResult.isErr()) { + // Propagate authentication errors + if (addToCollectionsResult.error instanceof AuthenticationError) { + return err(addToCollectionsResult.error); + } if ( addToCollectionsResult.error instanceof AppError.UnexpectedError ) { diff --git a/src/modules/cards/domain/services/CardCollectionService.ts b/src/modules/cards/domain/services/CardCollectionService.ts index 0d4c1aa6..2398f4c8 100644 --- a/src/modules/cards/domain/services/CardCollectionService.ts +++ b/src/modules/cards/domain/services/CardCollectionService.ts @@ -7,6 +7,7 @@ import { ICollectionRepository } from '../ICollectionRepository'; import { ICollectionPublisher } from '../../application/ports/ICollectionPublisher'; import { AppError } from '../../../../shared/core/AppError'; import { DomainService } from '../../../../shared/domain/DomainService'; +import { AuthenticationError } from '../../../../shared/core/AuthenticationError'; export class CardCollectionValidationError extends Error { constructor(message: string) { @@ -26,7 +27,7 @@ export class CardCollectionService implements DomainService { collectionId: CollectionId, curatorId: CuratorId, ): Promise< - Result + Result > { try { // Find the collection @@ -63,6 +64,10 @@ export class CardCollectionService implements DomainService { curatorId, ); if (publishLinkResult.isErr()) { + // Propagate authentication errors + if (publishLinkResult.error instanceof AuthenticationError) { + return err(publishLinkResult.error); + } return err( new CardCollectionValidationError( `Failed to publish collection link: ${publishLinkResult.error.message}`, @@ -93,7 +98,7 @@ export class CardCollectionService implements DomainService { ): Promise< Result< Collection[], - CardCollectionValidationError | AppError.UnexpectedError + CardCollectionValidationError | AuthenticationError | AppError.UnexpectedError > > { const updatedCollections: Collection[] = []; @@ -119,7 +124,7 @@ export class CardCollectionService implements DomainService { ): Promise< Result< Collection | null, - CardCollectionValidationError | AppError.UnexpectedError + CardCollectionValidationError | AuthenticationError | AppError.UnexpectedError > > { try { @@ -155,6 +160,10 @@ export class CardCollectionService implements DomainService { cardLink.publishedRecordId, ); if (unpublishLinkResult.isErr()) { + // Propagate authentication errors + if (unpublishLinkResult.error instanceof AuthenticationError) { + return err(unpublishLinkResult.error); + } return err( new CardCollectionValidationError( `Failed to unpublish collection link: ${unpublishLinkResult.error.message}`, @@ -193,7 +202,7 @@ export class CardCollectionService implements DomainService { ): Promise< Result< Collection[], - CardCollectionValidationError | AppError.UnexpectedError + CardCollectionValidationError | AuthenticationError | AppError.UnexpectedError > > { const updatedCollections: Collection[] = []; diff --git a/src/modules/cards/domain/services/CardLibraryService.ts b/src/modules/cards/domain/services/CardLibraryService.ts index 0d89ec0c..270897f5 100644 --- a/src/modules/cards/domain/services/CardLibraryService.ts +++ b/src/modules/cards/domain/services/CardLibraryService.ts @@ -8,6 +8,7 @@ import { AppError } from '../../../../shared/core/AppError'; import { DomainService } from '../../../../shared/domain/DomainService'; import { CardCollectionService } from './CardCollectionService'; import { PublishedRecordId } from '../value-objects/PublishedRecordId'; +import { AuthenticationError } from '../../../../shared/core/AuthenticationError'; export class CardLibraryValidationError extends Error { constructor(message: string) { @@ -28,7 +29,7 @@ export class CardLibraryService implements DomainService { card: Card, curatorId: CuratorId, ): Promise< - Result + Result > { try { // Check if card is already in curator's library @@ -76,6 +77,10 @@ export class CardLibraryService implements DomainService { parentCardPublishedRecordId, ); if (publishResult.isErr()) { + // Propagate authentication errors + if (publishResult.error instanceof AuthenticationError) { + return err(publishResult.error); + } return err( new CardLibraryValidationError( `Failed to publish card to library: ${publishResult.error.message}`, @@ -112,7 +117,7 @@ export class CardLibraryService implements DomainService { card: Card, curatorId: CuratorId, ): Promise< - Result + Result > { try { // Check if card is in curator's library @@ -190,6 +195,10 @@ export class CardLibraryService implements DomainService { libraryInfo.curatorId, ); if (unpublishResult.isErr()) { + // Propagate authentication errors + if (unpublishResult.error instanceof AuthenticationError) { + return err(unpublishResult.error); + } return err( new CardLibraryValidationError( `Failed to unpublish card from library: ${unpublishResult.error.message}`, diff --git a/src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts b/src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts index 3af1e84a..f5795602 100644 --- a/src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts +++ b/src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts @@ -2,6 +2,7 @@ import { Controller } from '../../../../../shared/infrastructure/http/Controller import { Response } from 'express'; import { AddUrlToLibraryUseCase } from '../../../application/useCases/commands/AddUrlToLibraryUseCase'; import { AuthenticatedRequest } from '../../../../../shared/infrastructure/http/middleware/AuthMiddleware'; +import { AuthenticationError } from '../../../../../shared/core/AuthenticationError'; export class AddUrlToLibraryController extends Controller { constructor(private addUrlToLibraryUseCase: AddUrlToLibraryUseCase) { @@ -29,12 +30,16 @@ export class AddUrlToLibraryController extends Controller { }); if (result.isErr()) { + // Check if the error is an authentication error + if (result.error instanceof AuthenticationError) { + return this.unauthorized(res, result.error.message); + } return this.fail(res, result.error); } return this.ok(res, result.value); } catch (error: any) { - return this.fail(res, error); + return this.handleError(res, error); } } } diff --git a/src/shared/core/AuthenticationError.ts b/src/shared/core/AuthenticationError.ts new file mode 100644 index 00000000..7eb0c6e3 --- /dev/null +++ b/src/shared/core/AuthenticationError.ts @@ -0,0 +1,6 @@ +export class AuthenticationError extends Error { + constructor(message: string) { + super(message); + this.name = 'AuthenticationError'; + } +} diff --git a/src/shared/infrastructure/http/Controller.ts b/src/shared/infrastructure/http/Controller.ts index e2b5c714..b91ad390 100644 --- a/src/shared/infrastructure/http/Controller.ts +++ b/src/shared/infrastructure/http/Controller.ts @@ -1,4 +1,5 @@ import { Response } from 'express'; +import { AuthenticationError } from '../../core/AuthenticationError'; export abstract class Controller { protected abstract executeImpl(req: any, res: Response): Promise; @@ -63,4 +64,11 @@ export abstract class Controller { message: error.toString(), }); } + + protected handleError(res: Response, error: Error): Response { + if (error instanceof AuthenticationError) { + return this.unauthorized(res, error.message); + } + return this.fail(res, error); + } }