@@ -4,6 +4,7 @@ import multipart from '@fastify/multipart';
|
|||||||
|
|
||||||
import { registerAuthRoutes } from '../modules/auth/routes/auth-routes.js';
|
import { registerAuthRoutes } from '../modules/auth/routes/auth-routes.js';
|
||||||
import { registerAdminInventoryRoutes } from '../modules/inventory/routes/admin-inventory-routes.js';
|
import { registerAdminInventoryRoutes } from '../modules/inventory/routes/admin-inventory-routes.js';
|
||||||
|
import { registerAdminLookupRoutes } from '../modules/inventory/routes/admin-lookup-routes.js';
|
||||||
import { registerAdminImageRoutes } from '../modules/inventory/routes/admin-image-routes.js';
|
import { registerAdminImageRoutes } from '../modules/inventory/routes/admin-image-routes.js';
|
||||||
import { registerPublicInventoryRoutes } from '../modules/inventory/routes/public-inventory-routes.js';
|
import { registerPublicInventoryRoutes } from '../modules/inventory/routes/public-inventory-routes.js';
|
||||||
import { registerPublicImageRoutes } from '../modules/inventory/routes/public-image-routes.js';
|
import { registerPublicImageRoutes } from '../modules/inventory/routes/public-image-routes.js';
|
||||||
@@ -48,6 +49,10 @@ export function buildApp(dependencies) {
|
|||||||
...dependencies,
|
...dependencies,
|
||||||
prefix: '/api/v1/admin',
|
prefix: '/api/v1/admin',
|
||||||
});
|
});
|
||||||
|
app.register(registerAdminLookupRoutes, {
|
||||||
|
...dependencies,
|
||||||
|
prefix: '/api/v1/admin',
|
||||||
|
});
|
||||||
app.register(registerAdminImageRoutes, {
|
app.register(registerAdminImageRoutes, {
|
||||||
...dependencies,
|
...dependencies,
|
||||||
prefix: '/api/v1/admin',
|
prefix: '/api/v1/admin',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { createPasswordResetRepository } from '../modules/auth/repositories/pass
|
|||||||
import { createRedisLoginRateLimiter } from '../modules/auth/services/login-rate-limiter.js';
|
import { createRedisLoginRateLimiter } from '../modules/auth/services/login-rate-limiter.js';
|
||||||
import { createPasswordRecoveryService } from '../modules/auth/services/password-recovery-service.js';
|
import { createPasswordRecoveryService } from '../modules/auth/services/password-recovery-service.js';
|
||||||
import { createInventoryItemRepository } from '../modules/inventory/repositories/inventory-item-repository.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 { createInventoryItemService } from '../modules/inventory/services/inventory-item-service.js';
|
||||||
import { createPublicInventoryService } from '../modules/inventory/services/public-inventory-service.js';
|
import { createPublicInventoryService } from '../modules/inventory/services/public-inventory-service.js';
|
||||||
import { createPublicImageService } from '../modules/inventory/services/public-image-service.js';
|
import { createPublicImageService } from '../modules/inventory/services/public-image-service.js';
|
||||||
@@ -51,6 +52,7 @@ async function startServer() {
|
|||||||
const redis = await createRedisConnection();
|
const redis = await createRedisConnection();
|
||||||
const productRepository = createProductRepository(database.db);
|
const productRepository = createProductRepository(database.db);
|
||||||
const inventoryItemRepository = createInventoryItemRepository(database.db);
|
const inventoryItemRepository = createInventoryItemRepository(database.db);
|
||||||
|
const catalogLookupRepository = createCatalogLookupRepository(database.db);
|
||||||
const inventoryImageRepository = createInventoryImageRepository(database.db);
|
const inventoryImageRepository = createInventoryImageRepository(database.db);
|
||||||
const imageDerivativeJobRepository = createImageDerivativeJobRepository(database.db);
|
const imageDerivativeJobRepository = createImageDerivativeJobRepository(database.db);
|
||||||
const listingRepository = createListingRepository(database.db);
|
const listingRepository = createListingRepository(database.db);
|
||||||
@@ -102,6 +104,7 @@ async function startServer() {
|
|||||||
inventoryItemRepository,
|
inventoryItemRepository,
|
||||||
productRepository,
|
productRepository,
|
||||||
}),
|
}),
|
||||||
|
catalogLookupRepository,
|
||||||
publicImageService,
|
publicImageService,
|
||||||
publicInventoryService: createPublicInventoryService({ inventoryItemRepository, publicImageService }),
|
publicInventoryService: createPublicInventoryService({ inventoryItemRepository, publicImageService }),
|
||||||
inventoryImageService: createInventoryImageService({
|
inventoryImageService: createInventoryImageService({
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { asc, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { categories, conditionProfiles, conditions } from '../../../infrastructure/database/schema.js';
|
||||||
|
|
||||||
|
export function createCatalogLookupRepository(db) {
|
||||||
|
return {
|
||||||
|
async listCategories() {
|
||||||
|
return db.select({ id: categories.id, name: categories.name }).from(categories).where(eq(categories.active, true)).orderBy(asc(categories.sort), asc(categories.name));
|
||||||
|
},
|
||||||
|
|
||||||
|
async listConditions() {
|
||||||
|
return db
|
||||||
|
.select({ id: conditions.id, name: conditions.name, code: conditions.code, categoryId: conditionProfiles.categoryId })
|
||||||
|
.from(conditions)
|
||||||
|
.innerJoin(conditionProfiles, eq(conditionProfiles.id, conditions.conditionProfileId))
|
||||||
|
.where(eq(conditions.active, true))
|
||||||
|
.orderBy(asc(conditions.qualityRank), asc(conditions.name));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export async function registerAdminLookupRoutes(app, options) {
|
||||||
|
const { authenticateAdmin, catalogLookupRepository } = options;
|
||||||
|
|
||||||
|
app.addHook('preHandler', async (request) => {
|
||||||
|
const admin = await authenticateAdmin(request);
|
||||||
|
if (!admin?.sellerId) {
|
||||||
|
const error = new Error('Authentication is required.');
|
||||||
|
error.code = 'UNAUTHORIZED';
|
||||||
|
error.statusCode = 401;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/lookups/categories', async () => ({ data: catalogLookupRepository.listCategories() }));
|
||||||
|
app.get('/lookups/conditions', async () => ({ data: catalogLookupRepository.listConditions() }));
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
|
|
||||||
import { useAuth } from '@/composables/useAuth.js';
|
import { useAuth } from '@/composables/useAuth.js';
|
||||||
import { createAdminInventory, createAdminProduct, listAdminInventory, listAdminProducts } from '@/services/admin-catalog-service.js';
|
import { createAdminInventory, createAdminProduct, listAdminCategories, listAdminConditions, listAdminInventory, listAdminProducts } from '@/services/admin-catalog-service.js';
|
||||||
|
|
||||||
const { principal } = useAuth();
|
const { principal } = useAuth();
|
||||||
const products = ref([]);
|
const products = ref([]);
|
||||||
const inventory = ref([]);
|
const inventory = ref([]);
|
||||||
|
const categories = ref([]);
|
||||||
|
const conditions = ref([]);
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
@@ -23,7 +25,7 @@ function csrfToken() {
|
|||||||
async function load() {
|
async function load() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
[products.value, inventory.value] = await Promise.all([listAdminProducts(), listAdminInventory()]);
|
[products.value, inventory.value, categories.value, conditions.value] = await Promise.all([listAdminProducts(), listAdminInventory(), listAdminCategories(), listAdminConditions()]);
|
||||||
} catch (requestError) { error.value = requestError; }
|
} catch (requestError) { error.value = requestError; }
|
||||||
finally { isLoading.value = false; }
|
finally { isLoading.value = false; }
|
||||||
}
|
}
|
||||||
@@ -63,16 +65,25 @@ onMounted(load);
|
|||||||
<h2>Add product</h2>
|
<h2>Add product</h2>
|
||||||
<label class="fixture-label" for="product-name">Name</label>
|
<label class="fixture-label" for="product-name">Name</label>
|
||||||
<input id="product-name" v-model="productName" class="fixture-input" required />
|
<input id="product-name" v-model="productName" class="fixture-input" required />
|
||||||
<label class="fixture-label" for="product-category">Category ID</label>
|
<label class="fixture-label" for="product-category">Category</label>
|
||||||
<input id="product-category" v-model="categoryId" class="fixture-input" required />
|
<select id="product-category" v-model="categoryId" class="fixture-input" required>
|
||||||
|
<option value="" disabled>Select a category</option>
|
||||||
|
<option v-for="category in categories" :key="category.id" :value="category.id">{{ category.name }}</option>
|
||||||
|
</select>
|
||||||
<button class="button button--primary" type="submit" :disabled="isSaving">Create product</button>
|
<button class="button button--primary" type="submit" :disabled="isSaving">Create product</button>
|
||||||
</form>
|
</form>
|
||||||
<form class="showcase-panel" @submit.prevent="createInventory">
|
<form class="showcase-panel" @submit.prevent="createInventory">
|
||||||
<h2>Add inventory copy</h2>
|
<h2>Add inventory copy</h2>
|
||||||
<label class="fixture-label" for="inventory-product">Product ID</label>
|
<label class="fixture-label" for="inventory-product">Product</label>
|
||||||
<input id="inventory-product" v-model="inventoryProductId" class="fixture-input" required />
|
<select id="inventory-product" v-model="inventoryProductId" class="fixture-input" required>
|
||||||
<label class="fixture-label" for="inventory-condition">Condition ID</label>
|
<option value="" disabled>Select a product</option>
|
||||||
<input id="inventory-condition" v-model="conditionId" class="fixture-input" required />
|
<option v-for="product in products" :key="product.id" :value="product.id">{{ product.name }}</option>
|
||||||
|
</select>
|
||||||
|
<label class="fixture-label" for="inventory-condition">Condition</label>
|
||||||
|
<select id="inventory-condition" v-model="conditionId" class="fixture-input" required>
|
||||||
|
<option value="" disabled>Select a condition</option>
|
||||||
|
<option v-for="condition in conditions" :key="condition.id" :value="condition.id">{{ condition.name }} ({{ condition.code }})</option>
|
||||||
|
</select>
|
||||||
<button class="button button--primary" type="submit" :disabled="isSaving">Create copy</button>
|
<button class="button button--primary" type="submit" :disabled="isSaving">Create copy</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ async function request(path, options = {}) {
|
|||||||
|
|
||||||
export function listAdminProducts() { return request('/admin/products'); }
|
export function listAdminProducts() { return request('/admin/products'); }
|
||||||
export function listAdminInventory() { return request('/admin/inventory'); }
|
export function listAdminInventory() { return request('/admin/inventory'); }
|
||||||
|
export function listAdminCategories() { return request('/admin/lookups/categories'); }
|
||||||
|
export function listAdminConditions() { return request('/admin/lookups/conditions'); }
|
||||||
|
|
||||||
export function createAdminProduct(input, csrfToken) {
|
export function createAdminProduct(input, csrfToken) {
|
||||||
return request('/admin/products', { method: 'POST', headers: { 'x-csrf-token': csrfToken }, body: JSON.stringify(input) });
|
return request('/admin/products', { method: 'POST', headers: { 'x-csrf-token': csrfToken }, body: JSON.stringify(input) });
|
||||||
|
|||||||
Reference in New Issue
Block a user