Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions app/[locale]/auth/reset-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ export default function ResetPasswordPage() {
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);

if (!token) {
return (
<AuthCard className={styles.centered}>
<h2 className={styles.title}>{dict.auth.resetPasswordTitle}</h2>
<ErrorMessage message={error ?? undefined} />
<Link href="/auth/forgot-password" className={styles.link}>
{dict.auth.requestNewLink}
</Link>
</AuthCard>
);
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
Expand Down Expand Up @@ -61,18 +73,6 @@ export default function ResetPasswordPage() {
}
};

if (!token) {
return (
<AuthCard className={styles.centered}>
<h2 className={styles.title}>{dict.auth.resetPasswordTitle}</h2>
<ErrorMessage message={error ?? undefined} />
<Link href="/auth/forgot-password" className={styles.link}>
{dict.auth.requestNewLink}
</Link>
</AuthCard>
);
}

return (
<AuthCard>
<h2 className={styles.title}>{dict.auth.resetPasswordTitle}</h2>
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/cart/design-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ function loadImage(src: string): Promise<HTMLImageElement | null> {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.addEventListener('load', () => resolve(img));
img.addEventListener('error', () => resolve(null));
img.src = src;
});
}
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/cart/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ export default async function CartPage({
productIds.map((id) => productRepository.findById(id, locale)),
);
const productMap = new Map<string, ProductEntity>();
products.forEach((p) => {
for (const p of products) {
if (p) productMap.set(p.id, p);
});
}

items = cart.items.map((item) => {
const product = productMap.get(item.productId.value);
Expand Down
4 changes: 2 additions & 2 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export async function generateMetadata({
const baseUrl = APP_BASE_URL;
const alternates: Record<string, string> = {};

['es', 'cat'].forEach((lang) => {
for (const lang of ['es', 'cat']) {
alternates[lang] = `${baseUrl}/${lang}`;
});
}

return {
metadataBase: new URL(baseUrl),
Expand Down
1 change: 0 additions & 1 deletion app/[locale]/products/[id]/customization-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export function CustomizationForm({
const config = ProductCustomizationConfig.fromJson(customizationConfig);

const textErrorId = errors.text ? 'customization-text-error' : undefined;
const _colorErrorId = errors.color ? 'customization-color-error' : undefined;
const sizeErrorId = errors.size ? 'customization-size-error' : undefined;

return (
Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/products/[id]/photo-upload-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function PhotoUploadField({

setError(null);
startTransition(() => {
void onUpload(file).then((result) => {
onUpload(file).then((result) => {
onUploaded?.(result);
});
});
Expand Down
10 changes: 5 additions & 5 deletions app/[locale]/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export default function ProfilePage() {
};
}, [status, locale, router]);

if (status === 'loading' || loading) {
return <div className={styles.loading}>{dict.common.loading}</div>;
}

const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
Expand Down Expand Up @@ -136,7 +140,7 @@ export default function ProfilePage() {
throw new Error(data.error || 'Failed to delete account');
}
// Redirect to home after soft-delete
window.location.href = '/';
window.location.assign('/');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to delete account');
} finally {
Expand All @@ -145,10 +149,6 @@ export default function ProfilePage() {
}
};

if (status === 'loading' || loading) {
return <div className={styles.loading}>{dict.common.loading}</div>;
}

return (
<div className={styles.container}>
<BackLink href={`/${locale}`}>{dict.common.backToHome}</BackLink>
Expand Down
22 changes: 13 additions & 9 deletions app/api/uploads/local/[...storageKey]/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { NextRequest, NextResponse } from 'next/server';

async function getStorageRoot(): Promise<string> {
const { join } = await import('node:path');
const path = await import('node:path');
return (
process.env.LOCAL_UPLOAD_STORAGE_DIR ??
join(process.cwd(), 'tmp', 'uploads')
path.join(process.cwd(), 'tmp', 'uploads')
);
}

async function resolveStoragePath(
storageKey: string[],
root: string,
): Promise<string | null> {
const { resolve, relative, isAbsolute } = await import('node:path');
const path = await import('node:path');

const decodedKey = decodeURIComponent(storageKey.join('/'));
if (
Expand All @@ -24,14 +24,18 @@ async function resolveStoragePath(
}
const segments = decodedKey.split('/').filter(Boolean);

const rawPath = resolve(root, ...segments);
const rawRelative = relative(root, rawPath);
if (rawRelative.startsWith('..') || isAbsolute(rawRelative)) {
const rawPath = path.resolve(root, ...segments);
const rawRelative = path.relative(root, rawPath);
if (
rawRelative === '..' ||
rawRelative.startsWith(`..${path.sep}`) ||
path.isAbsolute(rawRelative)
) {
return null;
}

const sanitized = segments.map((s) => s.replace(/[:*?"<>|]/g, '_'));
const filePath = resolve(root, ...sanitized);
const filePath = path.resolve(root, ...sanitized);

return filePath;
}
Expand All @@ -56,7 +60,7 @@ export async function PUT(
}

const { mkdir, writeFile } = await import('node:fs/promises');
const { dirname } = await import('node:path');
const path = await import('node:path');

const { storageKey } = await context.params;
const root = await getStorageRoot();
Expand All @@ -65,7 +69,7 @@ export async function PUT(
return NextResponse.json({ error: 'Invalid storage key' }, { status: 400 });
}

await mkdir(dirname(filePath), { recursive: true });
await mkdir(path.dirname(filePath), { recursive: true });

const body = Buffer.from(await request.arrayBuffer());
await writeFile(filePath, body);
Expand Down
9 changes: 4 additions & 5 deletions composition-root/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,10 @@ let _searchHistoryEventsSubscribed = false;
export function initContainer(): void {
// --- EmailSender: env-dependent (Brevo in production, console otherwise) ---
if (!_emailSender) {
if (process.env.NODE_ENV === 'production') {
_emailSender = new BrevoEmailSender();
} else {
_emailSender = new ConsoleEmailSender();
}
_emailSender =
process.env.NODE_ENV === 'production'
? new BrevoEmailSender()
: new ConsoleEmailSender();
}

// --- OutboxRepository: single Prisma adapter works in every env ---
Expand Down
15 changes: 1 addition & 14 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export default [
'unicorn/no-null': 'off', // muchos proyectos usan null intencionalmente (ej. React)
'unicorn/prefer-module': 'off', // si tenéis algún archivo CJS (configs, scripts)
'unicorn/name-replacements': 'off',
'unicorn/import-style': 'off',

'unicorn/no-top-level-assignment-in-function': 'off',
'unicorn/consistent-class-member-order': 'off',
Expand All @@ -176,20 +177,6 @@ export default [
'sonarjs/no-nested-template-literals': 'off',
'unicorn/no-nested-ternary': 'off',
'unicorn/no-array-sort': 'off',
'unicorn/prefer-number-is-safe-integer': 'off',
'unicorn/prefer-ternary': 'off',
'sonarjs/no-useless-react-setstate': 'off',
'sonarjs/no-hardcoded-passwords': 'off',
'unicorn/prefer-add-event-listener': 'off',
'sonarjs/super-linear-regex': 'off',
'unicorn/numeric-separators-style': 'off',
'sonarjs/void-use': 'off',
'unicorn/import-style': 'off',
'sonarjs/no-unused-vars': 'off',
'unicorn/no-declarations-before-early-exit': 'off',
'unicorn/prefer-string-raw': 'off',
'unicorn/no-for-each': 'off',
'unicorn/prefer-location-assign': 'off',

// SonarJS: ajustar el umbral de complejidad cognitiva si el default es muy estricto
// 'sonarjs/cognitive-complexity': ['warn', 15],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ export class MemoryUsedResetTokenStore implements UsedResetTokenStorePort {
/** Mark a token as used with optional expiry for auto-cleanup. */
markTokenUsed(jti: string, exp?: number): void {
this.purgeExpired();
this.usedTokens.set(jti, exp ?? Date.now() + 3600_000); // default 1h TTL
this.usedTokens.set(jti, exp ?? Date.now() + 3_600_000); // default 1h TTL
}
}
3 changes: 1 addition & 2 deletions modules/cart/application/add-item-to-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class AddItemToCart {
private async validateCustomizations(
customizationIdList: string[],
productId: string,
sellerId: string,
_sellerId: string,
): Promise<void> {
const snapshots =
await this.customizationLookup.findByIds(customizationIdList);
Expand All @@ -197,7 +197,6 @@ export class AddItemToCart {
// their sellerId from the Product. If productId matches, the
// sellerId is guaranteed to match (enforced at customization
// creation time by the customizations module).
void sellerId;
}
}

Expand Down
22 changes: 9 additions & 13 deletions modules/cart/application/checkout-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,7 @@ export class CheckoutCart {
) {}

async preview(userId: string): Promise<CheckoutPreview> {
const { cart, totals } = await this.buildTotals(userId, false);
// Touch the unused cart ref so TS doesn't complain about a return-only
// branch — the spec ties the preview to the cart's contents and we
// want the call to also serve as a validation gate.
void cart;
const { totals } = await this.buildTotals(userId, false);
return totals;
}

Expand Down Expand Up @@ -269,14 +265,14 @@ export class CheckoutCart {

// Persist the updated snapshots (if any) so the cart reflects the
// new prices. Status stays ACTIVE — checkout hasn't run yet.
let liveCart = cart;
if (acceptPriceChanges && priceChanges.length > 0) {
liveCart = await this.cartRepository.save({
...cart,
items: updatedItems,
updatedAt: new Date(),
});
}
const liveCart =
acceptPriceChanges && priceChanges.length > 0
? await this.cartRepository.save({
...cart,
items: updatedItems,
updatedAt: new Date(),
})
: cart;

// Compute totals from the live items (so acceptPriceChanges reflects
// the updated snapshot prices in subtotal/discount/total).
Expand Down
2 changes: 1 addition & 1 deletion modules/cart/domain/value-objects/quantity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class Quantity {
}

static create(amount: number): Quantity {
if (!Number.isFinite(amount) || !Number.isInteger(amount)) {
if (!Number.isSafeInteger(amount)) {
throw new InvalidQuantityError(
`Quantity must be a finite integer, got: ${amount}`,
);
Expand Down
10 changes: 6 additions & 4 deletions modules/cart/presentation/components/add-to-cart-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ export function AddToCartButton({
if (!isInCart || currentQuantity >= MAX_QUANTITY) return;
const newQty = currentQuantity + 1;
if (isAuthenticated && cartItemInfo) {
const prevCartItemInfo = cartItemInfo;
setCartItemInfo({ ...cartItemInfo, quantity: newQty });
try {
const res = await fetch(`/api/cart/items/${cartItemInfo.cartItemId}`, {
Expand All @@ -500,12 +501,12 @@ export function AddToCartButton({
body: JSON.stringify({ quantity: newQty }),
});
if (!res.ok) {
setCartItemInfo(cartItemInfo);
setCartItemInfo(prevCartItemInfo);
return;
}
dispatchCartUpdated();
} catch {
setCartItemInfo(cartItemInfo);
setCartItemInfo(prevCartItemInfo);
}
} else if (guestMatch?.id) {
updateItemQuantity(guestMatch.id, newQty);
Expand All @@ -525,6 +526,7 @@ export function AddToCartButton({

const newQty = currentQuantity - 1;
if (isAuthenticated && cartItemInfo) {
const prevCartItemInfo = cartItemInfo;
setCartItemInfo({ ...cartItemInfo, quantity: newQty });
try {
const res = await fetch(`/api/cart/items/${cartItemInfo.cartItemId}`, {
Expand All @@ -533,12 +535,12 @@ export function AddToCartButton({
body: JSON.stringify({ quantity: newQty }),
});
if (!res.ok) {
setCartItemInfo(cartItemInfo);
setCartItemInfo(prevCartItemInfo);
return;
}
dispatchCartUpdated();
} catch {
setCartItemInfo(cartItemInfo);
setCartItemInfo(prevCartItemInfo);
}
} else if (guestMatch?.id) {
updateItemQuantity(guestMatch.id, newQty);
Expand Down
2 changes: 1 addition & 1 deletion modules/cart/presentation/components/cart-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function CartIcon({ alt }: CartIconProps) {
useEffect(() => {
if (!isAuthenticated) return;
window.addEventListener(CART_UPDATED_EVENT, handleCartUpdated);
void Promise.try(fetchCount);
Promise.try(fetchCount);

return () => {
abortRef.current?.abort();
Expand Down
22 changes: 12 additions & 10 deletions modules/customizations/application/create-customer-customization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,6 @@ export class CreateCustomerCustomization {
config: ProductCustomizationConfig,
): void {
const hasText = dto.text !== undefined && dto.text !== null;
const hasImage = dto.imageUrl !== undefined && dto.imageUrl !== null;
const hasDesignPosition =
dto.designPosition !== undefined && dto.designPosition !== null;
// A canvas-only customization is also a "has image" signal — the
// mockup canvas embeds imageUrl inside designPosition, so a
// canvas-only draft must satisfy the photo requirement.
const hasImageForCapability = hasImage || hasDesignPosition;
const hasStyle =
(dto.color !== undefined && dto.color !== null) ||
(dto.size !== undefined && dto.size !== null);

if (hasText && !config.allowsText()) {
throw new ValidationError(
Expand All @@ -75,13 +65,25 @@ export class CreateCustomerCustomization {
);
}

const hasImage = dto.imageUrl !== undefined && dto.imageUrl !== null;
const hasDesignPosition =
dto.designPosition !== undefined && dto.designPosition !== null;
// A canvas-only customization is also a "has image" signal — the
// mockup canvas embeds imageUrl inside designPosition, so a
// canvas-only draft must satisfy the photo requirement.
const hasImageForCapability = hasImage || hasDesignPosition;

if (hasImageForCapability && !config.allowsPhoto()) {
throw new ValidationError(
'This product does not support photo customization',
'Customization is not allowed for this product',
);
}

const hasStyle =
(dto.color !== undefined && dto.color !== null) ||
(dto.size !== undefined && dto.size !== null);

if (hasStyle && !config.allowsStyleOptions()) {
throw new ValidationError(
'This product does not support color or size options',
Expand Down
Loading
Loading