import dotenv from "dotenv"; import { fileURLToPath } from "node:url"; dotenv.config({ path: fileURLToPath(new URL("../../../../.env", import.meta.url)), }); import { buildApp } from "./build-app.js"; import { createDatabase } from "../infrastructure/database/client.js"; import { resolveProjectImageStorageRoot } from "../infrastructure/images/local-image-storage.js"; import { createPasswordResetNotifier } from "../infrastructure/email/password-reset-notifier.js"; import { createRedisConnection } from "../infrastructure/redis/client.js"; import { createAuthenticationService } from "../modules/auth/services/authentication-service.js"; import { createMfaService } from "../modules/auth/services/mfa-service.js"; import { createSecretProtector } from "../modules/auth/domain/secret-protection.js"; import { createMfaMethodRepository } from "../modules/auth/repositories/mfa-method-repository.js"; import { createSecurityAuditRepository } from "../modules/auth/repositories/security-audit-repository.js"; import { createSessionRepository } from "../modules/auth/repositories/session-repository.js"; import { createUserRepository } from "../modules/auth/repositories/user-repository.js"; import { createPasswordResetRepository } from "../modules/auth/repositories/password-reset-repository.js"; import { createRedisLoginRateLimiter } from "../modules/auth/services/login-rate-limiter.js"; import { createPasswordRecoveryService } from "../modules/auth/services/password-recovery-service.js"; import { createInventoryItemRepository } from "../modules/inventory/repositories/inventory-item-repository.js"; import { createCatalogLookupRepository } from "../modules/inventory/repositories/catalog-lookup-repository.js"; import { createInventoryItemService } from "../modules/inventory/services/inventory-item-service.js"; import { createPublicInventoryService } from "../modules/inventory/services/public-inventory-service.js"; import { createPublicImageService } from "../modules/inventory/services/public-image-service.js"; import { createInventoryImageRepository } from "../modules/inventory/repositories/inventory-image-repository.js"; import { createImageDerivativeJobRepository } from "../modules/inventory/repositories/image-derivative-job-repository.js"; import { createInventoryImageService } from "../modules/inventory/services/inventory-image-service.js"; import { createLocalImageStorage } from "../infrastructure/images/local-image-storage.js"; import { createConditionRepository } from "../modules/inventory/repositories/condition-repository.js"; import { createListingRepository } from "../modules/listings/repositories/listing-repository.js"; import { createListingService } from "../modules/listings/services/listing-service.js"; import { createPublicListingService } from "../modules/listings/services/public-listing-service.js"; import { createProductRepository } from "../modules/products/repositories/product-repository.js"; import { createProductService } from "../modules/products/services/product-service.js"; import { createPublicProductService } from "../modules/products/services/public-product-service.js"; import { createSearchService } from "../modules/search/services/search-service.js"; import { createSellerRepository } from "../modules/sellers/repositories/seller-repository.js"; import { createPublicSellerService } from "../modules/sellers/services/public-seller-service.js"; import { createSaleRepository } from "../modules/sales/repositories/sale-repository.js"; import { createSaleService } from "../modules/sales/services/sale-service.js"; import { createAcquisitionRepository } from "../modules/acquisitions/repositories/acquisition-repository.js"; import { createAcquisitionService } from "../modules/acquisitions/services/acquisition-service.js"; import { createDashboardService } from "../modules/dashboard/services/dashboard-service.js"; import { createPricingRepository } from "../modules/pricing/repositories/pricing-repository.js"; import { createPricingService } from "../modules/pricing/services/pricing-service.js"; import { createPricingRefreshJobRepository } from "../modules/pricing/repositories/pricing-refresh-job-repository.js"; import { createProviderMappingRepository } from "../modules/pricing/repositories/provider-mapping-repository.js"; import { createProviderMappingService } from "../modules/pricing/services/provider-mapping-service.js"; import { createPricingProviderRegistry } from "../modules/pricing/providers/pricing-provider.js"; import { createFixturePricingProvider } from "../modules/pricing/providers/fixture-pricing-provider.js"; import { assertProductionPricingProviderConfig, createHttpPricingProvider, } from "../modules/pricing/providers/http-pricing-provider.js"; import { createAdminExportService } from "../modules/exports/services/admin-export-service.js"; import { createInventoryExportJobRepository } from "../modules/exports/repositories/inventory-export-job-repository.js"; import { createSellerSettingsRepository } from "../modules/settings/repositories/seller-settings-repository.js"; import { createSellerSettingsService } from "../modules/settings/services/seller-settings-service.js"; import { createSavedViewRepository } from "../modules/search/repositories/saved-view-repository.js"; import { createSavedViewService } from "../modules/search/services/saved-view-service.js"; function parsePort(value) { const port = Number.parseInt(value ?? "3000", 10); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error("API_PORT must be an integer between 1 and 65535."); } return port; } async function startServer() { if (process.env.NODE_ENV === "production") { assertProductionPricingProviderConfig({ fixtureEnabled: process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true", endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT, }); } const imageStorageRoot = resolveProjectImageStorageRoot( process.env.IMAGE_STORAGE_ROOT ?? ".local/image-storage", import.meta.url, ); const database = createDatabase(); const redis = await createRedisConnection(); const productRepository = createProductRepository(database.db); const inventoryItemRepository = createInventoryItemRepository(database.db); const catalogLookupRepository = createCatalogLookupRepository(database.db); const inventoryImageRepository = createInventoryImageRepository(database.db); const imageDerivativeJobRepository = createImageDerivativeJobRepository( database.db, ); const listingRepository = createListingRepository(database.db); const sellerRepository = createSellerRepository(database.db); const conditionRepository = createConditionRepository(database.db); const saleRepository = createSaleRepository(database.db); const acquisitionRepository = createAcquisitionRepository(database.db); const pricingRepository = createPricingRepository(database.db); const pricingRefreshJobRepository = createPricingRefreshJobRepository( database.db, ); const providerMappingRepository = createProviderMappingRepository( database.db, ); const sellerSettingsService = createSellerSettingsService({ sellerSettingsRepository: createSellerSettingsRepository(database.db), }); const savedViewService = createSavedViewService({ savedViewRepository: createSavedViewRepository(database.db), }); const providers = []; if (process.env.PRICING_FIXTURE_PROVIDER_ENABLED === "true") providers.push(createFixturePricingProvider()); if (process.env.PRICING_HTTP_PROVIDER_ENDPOINT) providers.push( createHttpPricingProvider({ id: process.env.PRICING_HTTP_PROVIDER_ID ?? "http-provider", endpoint: process.env.PRICING_HTTP_PROVIDER_ENDPOINT, token: process.env.PRICING_HTTP_PROVIDER_TOKEN, }), ); const pricingProviders = createPricingProviderRegistry(providers); const pricingService = createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders, }); const inventoryItemService = createInventoryItemService({ conditionRepository, inventoryItemRepository, pricingService, productRepository, }); const userRepository = createUserRepository(database.db); const sessionRepository = createSessionRepository(database.db); const passwordResetRepository = createPasswordResetRepository(database.db); const mfaMethodRepository = createMfaMethodRepository(database.db); const securityAuditRepository = createSecurityAuditRepository(database.db); const secretProtector = createSecretProtector(); const mfaService = createMfaService({ mfaMethodRepository, secretProtector, securityAuditRepository, }); const passwordResetNotifier = createPasswordResetNotifier(); const publicImageService = createPublicImageService({ imageStorage: createLocalImageStorage(imageStorageRoot), inventoryImageRepository, }); const authenticationService = createAuthenticationService({ mfaService, sessionRepository, securityAuditRepository, userRepository, }); const app = buildApp({ authenticationService, authenticateAdmin: (request) => authenticationService.authenticate( request.cookies.pokemon_inventory_session, ), authorizeUnsafeRequest: (sessionToken, csrfToken) => authenticationService.authorizeUnsafeRequest(sessionToken, csrfToken), isProduction: process.env.NODE_ENV === "production", loginRateLimiter: createRedisLoginRateLimiter({ redisClient: redis.client, }), mfaService, passwordRecoveryService: createPasswordRecoveryService({ passwordResetNotifier, passwordResetRepository, securityAuditRepository, userRepository, }), passwordResetRateLimiter: createRedisLoginRateLimiter({ keyPrefix: "pokemon-inventory:auth:password-reset:", redisClient: redis.client, }), inventoryItemService, adminExportService: createAdminExportService({ inventoryExportJobRepository: createInventoryExportJobRepository(database.db), inventoryItemService, }), sellerSettingsService, savedViewService, catalogLookupRepository, publicImageService, publicInventoryService: createPublicInventoryService({ inventoryItemRepository, publicImageService, }), inventoryImageService: createInventoryImageService({ imageStorage: createLocalImageStorage(imageStorageRoot), imageDerivativeJobRepository, inventoryImageRepository, }), listingService: createListingService({ listingRepository, inventoryImageRepository, }), saleService: createSaleService({ saleRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }), dashboardService: createDashboardService({ productService: createProductService({ productRepository }), inventoryItemService, listingService: createListingService({ listingRepository, inventoryImageRepository, }), saleService: createSaleService({ saleRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }), pricingRepository, pricingRefreshJobRepository, }), pricingService, providerMappingService: createProviderMappingService({ providerMappingRepository, }), publicListingService: createPublicListingService({ listingRepository, publicImageService, }), productService: createProductService({ productRepository }), publicProductService: createPublicProductService({ productRepository }), searchService: createSearchService({ listingRepository }), publicSellerService: createPublicSellerService({ sellerRepository }), }); app.addHook("onClose", async () => { await database.close(); await redis.close(); }); await app.listen({ host: "0.0.0.0", port: parsePort(process.env.API_PORT) }); } startServer().catch((error) => { console.error(error); process.exitCode = 1; });