Skip to content

Commit a204414

Browse files
Fix multi-tenant data leakage and remove hardcoded storeId in attribute endpoints (#4)
2 parents c00126d + 9871039 commit a204414

File tree

13 files changed

+174
-69
lines changed

13 files changed

+174
-69
lines changed

prisma/dev.db

8 KB
Binary file not shown.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Warnings:
3+
4+
- Added the required column `storeId` to the `ProductAttribute` table without a default value. This is not possible if the table is not empty.
5+
6+
*/
7+
-- First, get the first store ID to assign to existing attributes
8+
-- RedefineTables
9+
PRAGMA defer_foreign_keys=ON;
10+
PRAGMA foreign_keys=OFF;
11+
CREATE TABLE "new_ProductAttribute" (
12+
"id" TEXT NOT NULL PRIMARY KEY,
13+
"storeId" TEXT NOT NULL,
14+
"name" TEXT NOT NULL,
15+
"values" TEXT NOT NULL,
16+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
17+
"updatedAt" DATETIME NOT NULL,
18+
CONSTRAINT "ProductAttribute_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "Store" ("id") ON DELETE CASCADE ON UPDATE CASCADE
19+
);
20+
21+
-- Copy existing attributes and assign them to the first store
22+
INSERT INTO "new_ProductAttribute" ("createdAt", "id", "name", "updatedAt", "values", "storeId")
23+
SELECT "createdAt", "id", "name", "updatedAt", "values", (SELECT id FROM Store LIMIT 1) FROM "ProductAttribute";
24+
25+
DROP TABLE "ProductAttribute";
26+
ALTER TABLE "new_ProductAttribute" RENAME TO "ProductAttribute";
27+
CREATE INDEX "ProductAttribute_storeId_idx" ON "ProductAttribute"("storeId");
28+
CREATE INDEX "ProductAttribute_name_idx" ON "ProductAttribute"("name");
29+
CREATE UNIQUE INDEX "ProductAttribute_storeId_name_key" ON "ProductAttribute"("storeId", "name");
30+
PRAGMA foreign_keys=ON;
31+
PRAGMA defer_foreign_keys=OFF;

prisma/schema.sqlite.prisma

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ model Store {
251251
brands Brand[]
252252
orders Order[]
253253
customers Customer[]
254+
attributes ProductAttribute[]
254255
255256
createdAt DateTime @default(now())
256257
updatedAt DateTime @updatedAt
@@ -412,6 +413,9 @@ model Brand {
412413

413414
model ProductAttribute {
414415
id String @id @default(cuid())
416+
storeId String
417+
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
418+
415419
name String
416420
values String // JSON array of possible values
417421
@@ -420,6 +424,8 @@ model ProductAttribute {
420424
createdAt DateTime @default(now())
421425
updatedAt DateTime @updatedAt
422426
427+
@@unique([storeId, name])
428+
@@index([storeId])
423429
@@index([name])
424430
}
425431

src/app/api/attributes/[id]/route.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const updateAttributeSchema = z.object({
1313
});
1414

1515
// GET /api/attributes/[id] - Get attribute by ID
16-
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
16+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
1717
try {
1818
const session = await getServerSession(authOptions);
1919
if (!session?.user) {
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
2323
);
2424
}
2525

26-
const id = params?.id ?? request.nextUrl.pathname.split('/').pop();
26+
const { id } = await params;
2727
if (!id) {
2828
return NextResponse.json(
2929
{ error: 'Invalid request: missing id' },
@@ -44,7 +44,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
4444

4545
return NextResponse.json({ data: attribute });
4646
} catch (error: unknown) {
47-
console.error(`GET /api/attributes/${params?.id} error:`, error);
47+
console.error('GET /api/attributes/[id] error:', error);
4848
return NextResponse.json(
4949
{ error: 'Failed to fetch attribute', details: error instanceof Error ? error.message : String(error) },
5050
{ status: 500 }
@@ -53,7 +53,7 @@ export async function GET(request: NextRequest, { params }: { params: { id: stri
5353
}
5454

5555
// PATCH /api/attributes/[id] - Update attribute
56-
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
56+
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
5757
try {
5858
const session = await getServerSession(authOptions);
5959
if (!session?.user) {
@@ -66,7 +66,7 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
6666
const body = await request.json();
6767
const validatedData = updateAttributeSchema.parse(body);
6868

69-
const id = params?.id ?? request.nextUrl.pathname.split('/').pop();
69+
const { id } = await params;
7070
if (!id) {
7171
return NextResponse.json(
7272
{ error: 'Invalid request: missing id' },
@@ -85,7 +85,7 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
8585
message: 'Attribute updated successfully',
8686
});
8787
} catch (error: unknown) {
88-
console.error(`PATCH /api/attributes/${params?.id} error:`, error);
88+
console.error('PATCH /api/attributes/[id] error:', error);
8989

9090
if (error instanceof z.ZodError) {
9191
return NextResponse.json(
@@ -117,7 +117,7 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
117117
}
118118

119119
// DELETE /api/attributes/[id] - Delete attribute
120-
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
120+
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
121121
try {
122122
const session = await getServerSession(authOptions);
123123
if (!session?.user) {
@@ -127,7 +127,7 @@ export async function DELETE(request: NextRequest, { params }: { params: { id: s
127127
);
128128
}
129129

130-
const id = params?.id ?? request.nextUrl.pathname.split('/').pop();
130+
const { id } = await params;
131131
if (!id) {
132132
return NextResponse.json(
133133
{ error: 'Invalid request: missing id' },
@@ -142,7 +142,7 @@ export async function DELETE(request: NextRequest, { params }: { params: { id: s
142142
message: 'Attribute deleted successfully',
143143
});
144144
} catch (error: unknown) {
145-
console.error(`DELETE /api/attributes/${params?.id} error:`, error);
145+
console.error('DELETE /api/attributes/[id] error:', error);
146146

147147
if (error instanceof Error) {
148148
if (error.message.includes('not found')) {

src/app/api/attributes/route.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { z } from 'zod';
99

1010
// Validation schemas
1111
const listAttributesSchema = z.object({
12-
storeId: z.string().optional(),
12+
storeId: z.string().min(1, 'Store ID is required'),
1313
search: z.string().optional(),
1414
sortBy: z.enum(['name', 'createdAt', 'updatedAt']).optional(),
1515
sortOrder: z.enum(['asc', 'desc']).optional(),
@@ -47,14 +47,8 @@ export async function GET(request: NextRequest) {
4747
// Validate params
4848
const validatedParams = listAttributesSchema.parse(params);
4949

50-
// Default storeId to demo store if not provided
51-
const storeId = validatedParams.storeId || 'clqm1j4k00000l8dw8z8r8z8r';
52-
5350
const attributeService = AttributeService.getInstance();
54-
const result = await attributeService.listAttributes({
55-
...validatedParams,
56-
storeId,
57-
});
51+
const result = await attributeService.listAttributes(validatedParams);
5852

5953
return NextResponse.json(result);
6054
} catch (error) {

src/app/dashboard/attributes/[id]/page.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22
// Edit Attribute Page
33

44
import { getServerSession } from 'next-auth/next';
5-
import { redirect, notFound } from 'next/navigation';
5+
import { redirect } from 'next/navigation';
66
import { authOptions } from '@/lib/auth';
7-
import { AttributeForm } from '@/components/attribute-form';
87
import { AttributeEditClient } from '@/components/attribute-edit-client';
98

109
export const metadata = {
1110
title: 'Edit Attribute | Dashboard | StormCom',
1211
description: 'Edit product attribute',
1312
};
1413

15-
// No SSR data fetching to avoid Prisma runtime constraints; client layer will fetch
16-
1714
export default async function EditAttributePage({
1815
params,
1916
}: {
@@ -27,8 +24,6 @@ export default async function EditAttributePage({
2724

2825
const { id } = await params;
2926

30-
const storeId = 'clqm1j4k00000l8dw8z8r8z8r';
31-
3227
return (
3328
<div className="flex flex-col gap-4 p-4 md:p-6">
3429
<div>
@@ -38,7 +33,7 @@ export default async function EditAttributePage({
3833
</p>
3934
</div>
4035

41-
<AttributeEditClient id={id} storeId={storeId} />
36+
<AttributeEditClient id={id} />
4237
</div>
4338
);
4439
}

src/app/dashboard/attributes/new/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { getServerSession } from 'next-auth/next';
55
import { redirect } from 'next/navigation';
66
import { authOptions } from '@/lib/auth';
7-
import { AttributeForm } from '@/components/attribute-form';
7+
import { AttributeNewClient } from '@/components/attribute-new-client';
88

99
export const metadata = {
1010
title: 'New Attribute | Dashboard | StormCom',
@@ -18,9 +18,6 @@ export default async function NewAttributePage() {
1818
redirect('/login');
1919
}
2020

21-
// Use demo store ID for now
22-
const storeId = 'clqm1j4k00000l8dw8z8r8z8r';
23-
2421
return (
2522
<div className="flex flex-col gap-4 p-4 md:p-6">
2623
<div>
@@ -30,7 +27,7 @@ export default async function NewAttributePage() {
3027
</p>
3128
</div>
3229

33-
<AttributeForm storeId={storeId} />
30+
<AttributeNewClient />
3431
</div>
3532
);
3633
}

src/app/dashboard/attributes/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Suspense } from 'react';
55
import { getServerSession } from 'next-auth/next';
66
import { redirect } from 'next/navigation';
77
import { authOptions } from '@/lib/auth';
8-
import { AttributesTable } from '@/components/attributes-table';
8+
import { AttributesPageClient } from '@/components/attributes-page-client';
99
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
1010

1111
export const metadata = {
@@ -20,9 +20,6 @@ export default async function AttributesPage() {
2020
redirect('/login');
2121
}
2222

23-
// Use demo store ID for now
24-
const storeId = 'clqm1j4k00000l8dw8z8r8z8r';
25-
2623
return (
2724
<div className="flex flex-col gap-4 p-4 md:p-6">
2825
<div>
@@ -41,7 +38,7 @@ export default async function AttributesPage() {
4138
</CardHeader>
4239
<CardContent>
4340
<Suspense fallback={<div>Loading...</div>}>
44-
<AttributesTable storeId={storeId} />
41+
<AttributesPageClient />
4542
</Suspense>
4643
</CardContent>
4744
</Card>

src/components/attribute-edit-client.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33

44
import * as React from 'react';
55
import { AttributeForm } from '@/components/attribute-form';
6+
import { StoreSelector } from '@/components/store-selector';
67
import { toast } from 'sonner';
78

89
interface AttributeEditClientProps {
910
id: string;
10-
storeId: string;
1111
}
1212

13-
export function AttributeEditClient({ id, storeId }: AttributeEditClientProps) {
13+
export function AttributeEditClient({ id }: AttributeEditClientProps) {
14+
const [storeId, setStoreId] = React.useState<string>('');
1415
const [loading, setLoading] = React.useState(true);
1516
const [error, setError] = React.useState<string | null>(null);
1617
const [data, setData] = React.useState<{ name: string; values: string[] } | null>(null);
1718

1819
React.useEffect(() => {
20+
if (!storeId) return;
1921
let active = true;
2022
(async () => {
2123
try {
@@ -37,20 +39,32 @@ export function AttributeEditClient({ id, storeId }: AttributeEditClientProps) {
3739
}
3840
})();
3941
return () => { active = false; };
40-
}, [id]);
41-
42-
if (loading) {
43-
return <p className="text-muted-foreground">Loading attribute...</p>;
44-
}
45-
if (error || !data) {
46-
return <p className="text-red-600">{error ?? 'Attribute not found'}</p>;
47-
}
42+
}, [id, storeId]);
4843

4944
return (
50-
<AttributeForm
51-
attributeId={id}
52-
initialData={data}
53-
storeId={storeId}
54-
/>
45+
<div className="space-y-6">
46+
<div className="flex items-center gap-4">
47+
<label className="text-sm font-medium">Store:</label>
48+
<StoreSelector onStoreChange={setStoreId} />
49+
</div>
50+
51+
{!storeId ? (
52+
<div className="rounded-lg border bg-card p-12 text-center">
53+
<p className="text-sm text-muted-foreground">
54+
Select a store to edit the attribute
55+
</p>
56+
</div>
57+
) : loading ? (
58+
<p className="text-muted-foreground">Loading attribute...</p>
59+
) : error || !data ? (
60+
<p className="text-red-600">{error ?? 'Attribute not found'}</p>
61+
) : (
62+
<AttributeForm
63+
attributeId={id}
64+
initialData={data}
65+
storeId={storeId}
66+
/>
67+
)}
68+
</div>
5569
);
5670
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
// src/components/attribute-new-client.tsx
4+
// Client component for creating new attribute with store selection
5+
6+
import { useState } from 'react';
7+
import { StoreSelector } from '@/components/store-selector';
8+
import { AttributeForm } from '@/components/attribute-form';
9+
10+
export function AttributeNewClient() {
11+
const [storeId, setStoreId] = useState<string>('');
12+
13+
return (
14+
<div className="space-y-6">
15+
<div className="flex items-center gap-4">
16+
<label className="text-sm font-medium">Store:</label>
17+
<StoreSelector onStoreChange={setStoreId} />
18+
</div>
19+
20+
{storeId ? (
21+
<AttributeForm storeId={storeId} />
22+
) : (
23+
<div className="rounded-lg border bg-card p-12 text-center">
24+
<p className="text-sm text-muted-foreground">
25+
Select a store to create an attribute
26+
</p>
27+
</div>
28+
)}
29+
</div>
30+
);
31+
}

0 commit comments

Comments
 (0)