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:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user