Skip to content

Commit 0eb63a5

Browse files
committed
uppp
1 parent 79e8ac5 commit 0eb63a5

15 files changed

Lines changed: 1353 additions & 74 deletions

File tree

prisma/dev.db

24 KB
Binary file not shown.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- CreateTable
2+
CREATE TABLE "store_domains" (
3+
"id" TEXT NOT NULL PRIMARY KEY,
4+
"storeId" TEXT NOT NULL,
5+
"domain" TEXT NOT NULL,
6+
"isPrimary" BOOLEAN NOT NULL DEFAULT false,
7+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
"updatedAt" DATETIME NOT NULL,
9+
CONSTRAINT "store_domains_storeId_fkey" FOREIGN KEY ("storeId") REFERENCES "stores" ("id") ON DELETE CASCADE ON UPDATE CASCADE
10+
);
11+
12+
-- CreateIndex
13+
CREATE UNIQUE INDEX "store_domains_domain_key" ON "store_domains"("domain");
14+
15+
-- CreateIndex
16+
CREATE INDEX "store_domains_storeId_idx" ON "store_domains"("storeId");
17+
18+
-- CreateIndex
19+
CREATE INDEX "store_domains_domain_idx" ON "store_domains"("domain");

prisma/schema.prisma

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ model Store {
285285
webhooks Webhook[]
286286
gdprRequests GdprRequest[]
287287
consentRecords ConsentRecord[]
288+
domains StoreDomain[]
288289
289290
@@index([slug])
290291
@@index([subscriptionPlan])
@@ -295,6 +296,28 @@ model Store {
295296
@@map("stores")
296297
}
297298

299+
// ============================================================================
300+
// STORE DOMAINS (Multi-tenancy)
301+
// ============================================================================
302+
303+
model StoreDomain {
304+
id String @id @default(cuid())
305+
storeId String
306+
store Store @relation(fields: [storeId], references: [id], onDelete: Cascade)
307+
308+
// Domain configuration
309+
domain String @unique // e.g., "shop.example.com" or "example.com"
310+
isPrimary Boolean @default(false) // Canonical domain for redirects
311+
312+
// Timestamps
313+
createdAt DateTime @default(now())
314+
updatedAt DateTime @updatedAt
315+
316+
@@index([storeId])
317+
@@index([domain])
318+
@@map("store_domains")
319+
}
320+
298321
// ============================================================================
299322
// PRODUCTS & CATALOG
300323
// ============================================================================

prisma/seed.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,53 @@ async function main() {
8484

8585
console.log(`✓ Created stores: ${demoStore.name}, ${testStore.name}\n`);
8686

87+
// ============================================================================
88+
// 2a. STORE DOMAINS
89+
// ============================================================================
90+
console.log('Creating store domains...');
91+
92+
await prisma.storeDomain.upsert({
93+
where: { domain: 'demo.stormcom.io' },
94+
update: {},
95+
create: {
96+
storeId: demoStore.id,
97+
domain: 'demo.stormcom.io',
98+
isPrimary: true,
99+
},
100+
});
101+
102+
await prisma.storeDomain.upsert({
103+
where: { domain: 'demo-store.localhost' },
104+
update: {},
105+
create: {
106+
storeId: demoStore.id,
107+
domain: 'demo-store.localhost',
108+
isPrimary: false,
109+
},
110+
});
111+
112+
await prisma.storeDomain.upsert({
113+
where: { domain: 'test.stormcom.io' },
114+
update: {},
115+
create: {
116+
storeId: testStore.id,
117+
domain: 'test.stormcom.io',
118+
isPrimary: true,
119+
},
120+
});
121+
122+
await prisma.storeDomain.upsert({
123+
where: { domain: 'test-store.localhost' },
124+
update: {},
125+
create: {
126+
storeId: testStore.id,
127+
domain: 'test-store.localhost',
128+
isPrimary: false,
129+
},
130+
});
131+
132+
console.log(`✓ Created store domains for: ${demoStore.name}, ${testStore.name}\n`);
133+
87134
// ============================================================================
88135
// 3. STORE USERS
89136
// ============================================================================

proxy.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,19 +147,23 @@ const SECURITY_HEADERS = {
147147
/**
148148
* Apply security protections to the request
149149
*
150-
* Handles multi-tenant context, rate limiting, CSRF protection, and security headers.
150+
* Handles multi-tenant context preparation, rate limiting, CSRF protection, and security headers.
151151
* Exported for testing purposes.
152152
*
153+
* Note: Proxy runs in Edge runtime - cannot use Prisma or AsyncLocalStorage.
154+
* Store resolution must happen in API routes/Server Components using resolveStore().
155+
*
153156
* @param request - NextRequest to protect
154157
* @returns NextResponse with security headers applied
155158
*/
156159
export async function applySecurityProtections(request: NextRequest): Promise<NextResponse> {
157160
const { method, url } = request;
158161
const { pathname } = new URL(url);
159162

160-
// 0. Multi-tenant Context (removed in proxy):
161-
// Do not attempt to access getServerSession or Prisma in proxy (Edge runtime).
162-
// Tenant context is enforced within server routes and Prisma middleware.
163+
// 0. Multi-tenant Context Preparation:
164+
// Proxy sets X-Forwarded-Host header for API routes/Server Components to resolve storeId.
165+
// Actual resolution happens in server context using resolveStore() from @/lib/store/resolve-store
166+
// due to Edge runtime limitations (no Prisma, no AsyncLocalStorage).
163167

164168
// 1. Rate Limiting: Check request limits
165169
const rateLimitResult = checkSimpleRateLimit(request);
@@ -191,6 +195,10 @@ export async function applySecurityProtections(request: NextRequest): Promise<Ne
191195
// 3. Security Headers: Apply to all responses
192196
const response = NextResponse.next();
193197

198+
// Set X-Forwarded-Host for store resolution in API routes
199+
const host = request.headers.get('host') || '';
200+
response.headers.set('x-forwarded-host', host);
201+
194202
// Apply all security headers
195203
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
196204
response.headers.set(key, value);
@@ -215,6 +223,12 @@ console.log('[PROXY] Module loaded');
215223
export default async function proxy(req: NextRequest) {
216224
const { pathname } = req.nextUrl;
217225

226+
// 0. Canonical Domain Redirect
227+
// Note: Actual canonical redirect happens in storefront Server Components via
228+
// checkCanonicalRedirect() from @/lib/store/canonical-redirect
229+
// Proxy only sets x-forwarded-host header for server-side resolution
230+
// (Edge runtime cannot access Prisma for StoreDomain lookup)
231+
218232
// Import RBAC helpers lazily to keep edge bundle small
219233
const { canAccess, isPublicRoute, getDefaultRedirect } = await import('./src/lib/auth/permissions');
220234

0 commit comments

Comments
 (0)