feat: establish auth listing and image vertical slices

Includes MFA-capable authentication, session and CSRF security, password recovery, listing lifecycle and ownership rules, inventory image metadata, local ImageStorage, migrations, tests, and implementation status documentation. Verification: npm test (62 passing) and integration suite (14 passing).
This commit is contained in:
2026-09-08 17:32:03 -04:00
commit c728016dec
128 changed files with 9788 additions and 0 deletions
View File
+23
View File
@@ -0,0 +1,23 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toSessionDto(session, currentSessionId) {
return {
id: session.id,
createdAt: toIsoString(session.createdAt),
expiresAt: toIsoString(session.expiresAt),
lastActivityAt: toIsoString(session.lastActivityAt),
ipAddress: session.ipAddress,
userAgent: session.userAgent,
isCurrent: session.id === currentSessionId,
};
}
export function toMfaMethodDto(method) {
return {
id: method.id,
name: method.name,
type: method.type,
};
}
+22
View File
@@ -0,0 +1,22 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toAdminInventoryImageDto(image) {
return {
id: image.id,
inventoryItemId: image.inventoryItemId,
storageKey: image.storageKey,
originalFilename: image.originalFilename,
mimeType: image.mimeType,
fileSize: image.fileSize,
width: image.width,
height: image.height,
altText: image.altText ?? null,
sortOrder: image.sortOrder,
isPrimary: image.isPrimary,
processingStatus: image.processingStatus,
createdAt: toIsoString(image.createdAt),
updatedAt: toIsoString(image.updatedAt),
};
}
+15
View File
@@ -0,0 +1,15 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toAdminInventoryItemDto(inventoryItem) {
return {
id: inventoryItem.id,
productId: inventoryItem.productId,
conditionId: inventoryItem.conditionId,
status: inventoryItem.status,
notes: inventoryItem.notes ?? null,
createdAt: toIsoString(inventoryItem.createdAt),
updatedAt: toIsoString(inventoryItem.updatedAt),
};
}
+19
View File
@@ -0,0 +1,19 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toAdminListingDto(listing) {
return {
id: listing.id,
inventoryItemId: listing.inventoryItemId,
productName: listing.productName,
categoryId: listing.categoryId,
conditionId: listing.conditionId,
askingPriceMinor: listing.askingPriceMinor,
imageCount: listing.imageCount,
status: listing.status,
notes: listing.notes ?? null,
createdAt: toIsoString(listing.createdAt),
updatedAt: toIsoString(listing.updatedAt),
};
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "@pokemon-inventory/contracts",
"private": true,
"type": "module",
"exports": {
"./auth": "./auth.js",
"./inventory": "./inventory.js",
"./image": "./image.js",
"./listing": "./listing.js",
"./product": "./product.js"
}
}
+21
View File
@@ -0,0 +1,21 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toAdminProductDto(product) {
return {
id: product.id,
sellerId: product.sellerId,
categoryId: product.categoryId,
name: product.name,
slug: product.slug,
description: product.description ?? null,
brand: product.brand ?? null,
setName: product.setName ?? null,
cardNumber: product.cardNumber ?? null,
eventName: product.eventName ?? null,
releaseYear: product.releaseYear ?? null,
createdAt: toIsoString(product.createdAt),
updatedAt: toIsoString(product.updatedAt),
};
}
View File
View File
+34
View File
@@ -0,0 +1,34 @@
import { z } from 'zod';
export const loginSchema = z
.object({
email: z.string().trim().email().max(320),
password: z.string().min(1).max(1024),
mfaCode: z.string().regex(/^\d{6}$/).optional(),
rememberMe: z.boolean().optional(),
})
.strict();
export const sessionIdSchema = z.uuid();
export const requestPasswordResetSchema = z
.object({ email: z.string().trim().email().max(320) })
.strict();
export const resetPasswordSchema = z
.object({
token: z.string().min(32).max(512),
password: z.string().min(12).max(1024),
})
.strict();
export const enrollMfaSchema = z
.object({ name: z.string().trim().min(1).max(100) })
.strict();
export const verifyMfaSchema = z
.object({
methodId: z.uuid(),
code: z.string().regex(/^\d{6}$/),
})
.strict();
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod';
export const createInventoryImageSchema = z.object({
storageKey: z.string().trim().min(1).max(500),
originalFilename: z.string().trim().min(1).max(255),
mimeType: z.enum(['image/jpeg', 'image/png', 'image/webp']),
fileSize: z.int().positive(),
width: z.int().positive(),
height: z.int().positive(),
altText: z.string().trim().max(1000).optional(),
sortOrder: z.int().nonnegative().optional(),
isPrimary: z.boolean().optional(),
}).strict();
+9
View File
@@ -0,0 +1,9 @@
import { z } from 'zod';
export const createInventoryItemSchema = z
.object({
productId: z.uuid(),
conditionId: z.uuid(),
notes: z.string().trim().max(10000).optional(),
})
.strict();
+13
View File
@@ -0,0 +1,13 @@
import { z } from 'zod';
export const createListingSchema = z
.object({
inventoryItemId: z.uuid(),
productName: z.string().trim().min(1).max(255),
categoryId: z.uuid(),
conditionId: z.uuid(),
askingPriceMinor: z.int().positive(),
imageCount: z.int().positive(),
notes: z.string().trim().max(10000).optional(),
})
.strict();
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@pokemon-inventory/validation",
"private": true,
"type": "module",
"exports": {
"./auth": "./auth.js",
"./inventory": "./inventory.js",
"./image": "./image.js",
"./listing": "./listing.js",
"./product": "./product.js"
},
"dependencies": {
"zod": "^4.1.8"
}
}
+10
View File
@@ -0,0 +1,10 @@
import { z } from 'zod';
const identifier = z.uuid();
export const createProductSchema = z
.object({
name: z.string().trim().min(1).max(255),
categoryId: identifier,
})
.strict();