Files
pokemon-inventory/apps/api/src/app/server.js
T
2026-09-09 13:01:16 -04:00

162 lines
9.4 KiB
JavaScript

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';
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() {
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 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: createInventoryItemService({
conditionRepository,
inventoryItemRepository,
productRepository,
}),
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: createInventoryItemService({ conditionRepository, inventoryItemRepository, productRepository }),
listingService: createListingService({ listingRepository, inventoryImageRepository }),
saleService: createSaleService({ saleRepository }),
acquisitionService: createAcquisitionService({ acquisitionRepository }),
}),
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository }),
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;
});