7.3 KiB
7.3 KiB
ADM-001 / ADM-002 / ADM-003 — Foundation Spec
Goal
Establecer el proyecto Next.js Admin, el API Client tipado, y el flujo de autenticación completo.
1. Proyecto apps/admin
Stack
- Next.js 16 (App Router)
- TypeScript (strict mode)
- Tailwind CSS (extend del config existente en
frontend/) - URL:
http://localhost:3004(evitar conflicto con frontend :3003 y backend :3000)
package.json
{
"name": "@mercadodevida/admin",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3004",
"build": "next build",
"start": "next start --port 3004",
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.6.0",
"tailwindcss": "^4.0.0",
"@tailwindcss/postcss": "^4.0.0",
"eslint": "^9.0.0",
"@eslint/eslintrc": "^3.0.0"
}
}
Configuración compartida
tailwind.config.tsextiende los colores de marca del storefrontnext.config.tsignora TypeScript errors en build.env.local:NEXT_PUBLIC_API_URL=http://127.0.0.1:3000
Estructura inicial
apps/admin/
├── app/
│ ├── (auth)/
│ │ └── login/
│ │ └── page.tsx
│ ├── (dashboard)/
│ │ └── page.tsx # redirect to /admin/products
│ └── layout.tsx
├── lib/
│ ├── api-client.ts
│ └── permissions.ts
├── types/
│ └── index.ts
├── package.json
├── tsconfig.json
├── next.config.ts
└── tailwind.config.ts
2. API Client (lib/api-client.ts)
Interfaz
interface ApiClient {
get<T>(path: string, params?: Record<string, string | number>): Promise<T>;
post<T>(path: string, body?: unknown): Promise<T>;
patch<T>(path: string, body?: unknown): Promise<T>;
delete<T>(path: string): Promise<T>;
}
interface ApiError {
statusCode: number;
code: string;
message: string;
}
Comportamiento
- Añade
Cookie: session_token=<token>a cada request desdedocument.cookie - POST/PATCH/DELETE van con
credentials: include - En error 401 → limpia cookie →
window.location.href = '/login' - En error 403 → lanza
ForbiddenError - En error 404 → lanza
NotFoundError - En error >= 500 → lanza
ServerError - Otros errores → mapea
error.messagedel body
Feature adapters
// lib/api/products.ts
export const productsApi = {
list: (params: ProductListParams) => client.get<Product[]>('/products/search', params),
get: (id: string) => client.get<Product>(`/products/${id}`),
create: (data: CreateProductInput) => client.post<Product>('/products', data),
update: (id: string, data: UpdateProductInput) => client.patch<Product>(`/products/${id}`, data),
updateVariants: (productId, variantId, data) =>
client.patch(`/products/${productId}/variants/${variantId}`, data),
};
// lib/api/orders.ts
export const ordersApi = {
list: (params?: OrderListParams) => client.get<Order[]>('/orders', params),
get: (id: string) => client.get<Order>(`/orders/${id}/admin`),
transition: (id: string, state: string) =>
client.post<Order>(`/orders/${id}/transitions/admin`, { state }),
};
// lib/api/auth.ts
export const authApi = {
login: (email, password) => client.post<{ user: User }>('/auth/login', { email, password }),
logout: () => client.post('/auth/logout'),
me: () => client.get<{ user: User } | { user: null }>('/auth/me'),
};
Tipos (types/index.ts)
// Productos
interface Product { id, name, slug, description, state, brandId, categoryIds, images, ... }
interface ProductVariant { id, productId, sku, ean, attributes }
interface VariantPrice { variantId, netUnitAmountCents, vatRate, currency }
interface StockAvailability { available, availableQuantity }
// Órdenes
interface Order {
id, userId, state, currency,
subtotalCents, discountCents, taxCents, totalCents,
idempotencyKey, createdAt, updatedAt,
items: OrderItem[]
}
interface OrderItem { id, productId, variantId, sku, ean, name, unitPriceCents, discountCents, taxCents, quantity }
// Usuarios/Clientes
interface User { id, email, role, createdAt }
// Errores
interface ApiError { statusCode, code, message }
3. Auth Flow ((auth)/login/page.tsx)
UX Login
- Server Component con
"use client"para el form - Campos: email + password
- POST a
/api/auth/login(Next.js API route proxy) → backend - Cookie de sesión viene del backend en
Set-Cookie - Éxito: redirect a
/admin/products - Error: mensaje de error inline
API Route proxy (app/api/auth/login/route.ts)
// Forward al backend, propagate Set-Cookie
export async function POST(req: Request) {
const body = await req.json();
const backendRes = await fetch('http://127.0.0.1:3000/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await backendRes.json();
if (!backendRes.ok) return Response.json(data, { status: backendRes.status });
const resp = Response.json(data);
const setCookie = backendRes.headers.get('set-cookie');
if (setCookie) resp.headers.set('Set-Cookie', setCookie);
return resp;
}
Mismo patrón para /api/auth/logout y /api/auth/me.
Middleware de protección
// middleware.ts
export function middleware(req: NextRequest) {
const session = req.cookies.get('session_token');
if (!session && !req.nextUrl.pathname.startsWith('/login')) {
return NextResponse.redirect(new URL('/login', req.url));
}
}
4. Permissions
// lib/permissions.ts
type Permission = 'products.read' | 'products.write' | 'orders.read' | 'orders.write' | ...;
export function can(role: Role, permission: Permission): boolean {
if (role === 'admin') return true;
// Future: granular permissions
return false;
}
export const NAV_ITEMS = [
{ href: '/admin/products', label: 'Productos', icon: '📦', permission: 'products.read' },
{ href: '/admin/orders', label: 'Pedidos', icon: '🧾', permission: 'orders.read' },
// ...
];
5. Acceptance Criteria
ADM-001
apps/admin/compila connpm run buildnpm run devlevanta en puerto 3004- Tailwind usa los colores de marca de MercadoDeVida
ADM-002
ApiClienthace requests con cookie de sesión- Error 401 redirige a login
- Todos los feature adapters tipados
can(role, permission)funciona
ADM-003
- Login con credenciales válidas →
/admin/products - Login con credenciales inválidas → mensaje de error
- Recargar
/admin/productscon sesión válida → mantiene página - Recargar
/admin/productssin sesión →/admin/login - Logout →
/admin/login
6. Tests
Unit
api-client.test.ts: mock fetch, verificar request/response mappingpermissions.test.ts: can() para admin y customer
E2E (Playwright)
login-success.spec.ts: login → dashboardlogin-failure.spec.ts: credenciales inválidas → errorsession-persistence.spec.ts: recargar mantiene sesiónlogout.spec.ts: logout → login