Skip to content

Commit e6480db

Browse files
Merge pull request #179 from DevLoversTeam/lso/feat/shop
(P1: 3) [Security/Infra] Enforce origin posture, harden DB integrity constraints, and finalize shop observability safety (no console, correlation IDs)
2 parents 1df5a40 + de6793a commit e6480db

51 files changed

Lines changed: 6212 additions & 849 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# --- Core / Environment
2+
APP_ADDITIONAL_ORIGINS=https://admin.example.test
23
APP_ENV=
4+
APP_ORIGIN=https://example.test
35
APP_URL=
46
NEXT_PUBLIC_SITE_URL=
57

@@ -83,6 +85,11 @@ STRIPE_WEBHOOK_RL_WINDOW_SECONDS=60
8385
STRIPE_WEBHOOK_INVALID_SIG_RL_MAX=30
8486
STRIPE_WEBHOOK_INVALID_SIG_RL_WINDOW_SECONDS=60
8587

88+
# SECURITY: If true, trust Cloudflare's cf-connecting-ip header for rate limiting.
89+
# Enable ONLY when traffic is fronted by Cloudflare (header is set by Cloudflare at the edge).
90+
# Default: false (0). Keep 0 in untrusted environments to avoid IP spoofing.
91+
TRUST_CF_CONNECTING_IP=0
92+
8693
# SECURITY: If true, trust x-real-ip / x-forwarded-for headers for rate limiting.
8794
# Enable ONLY behind Cloudflare or a trusted reverse proxy that overwrites these headers.
8895
# Default: false (empty/0/false).

frontend/.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,8 @@ next-env.d.ts
4646
# Documentation (only for development)
4747
CLAUDE.md
4848
docs/
49-
.claude
49+
.claude
50+
51+
!docs/
52+
!docs/security/
53+
!docs/security/origin-posture.md

frontend/app/[locale]/shop/admin/orders/page.tsx

Lines changed: 175 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ export default async function AdminOrdersPage({
6161
const hasNext = all.length > PAGE_SIZE;
6262
const items = all.slice(0, PAGE_SIZE);
6363

64+
const viewModels = items.map(order => {
65+
const currency = orderCurrency(order, locale);
66+
const totalMinor = pickMinor(order?.totalAmountMinor, order?.totalAmount);
67+
68+
return {
69+
id: order.id,
70+
createdAt: formatDate(order.createdAt, locale),
71+
paymentStatus: order.paymentStatus,
72+
totalFormatted:
73+
totalMinor === null ? '-' : formatMoney(totalMinor, currency, locale),
74+
itemCount: order.itemCount,
75+
paymentProvider: order.paymentProvider ?? '-',
76+
viewHref: `/shop/admin/orders/${order.id}`,
77+
viewAriaLabel: `View order ${order.id}`,
78+
};
79+
});
80+
6481
return (
6582
<>
6683
<ShopAdminTopbar />
@@ -69,7 +86,7 @@ export default async function AdminOrdersPage({
6986
className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8"
7087
aria-labelledby="admin-orders-title"
7188
>
72-
<header className="flex items-start justify-between gap-4">
89+
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
7390
<h1
7491
id="admin-orders-title"
7592
className="text-2xl font-bold text-foreground"
@@ -81,127 +98,195 @@ export default async function AdminOrdersPage({
8198
<input type="hidden" name={CSRF_FORM_FIELD} value={csrfToken} />
8299
<button
83100
type="submit"
84-
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
101+
className="inline-flex w-full items-center justify-center rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary sm:w-auto"
85102
>
86103
Reconcile stale
87104
</button>
88105
</form>
89106
</header>
90107

91-
<section className="mt-6" aria-label="Orders table">
92-
<div className="overflow-x-auto">
93-
<table className="min-w-full divide-y divide-border text-sm">
94-
<caption className="sr-only">Orders list</caption>
95-
96-
<thead className="bg-muted/50">
97-
<tr>
98-
<th
99-
scope="col"
100-
className="px-3 py-2 text-left font-semibold text-foreground"
101-
>
102-
Created
103-
</th>
104-
<th
105-
scope="col"
106-
className="px-3 py-2 text-left font-semibold text-foreground"
107-
>
108-
Status
109-
</th>
110-
<th
111-
scope="col"
112-
className="px-3 py-2 text-left font-semibold text-foreground"
108+
<section className="mt-6" aria-label="Orders list">
109+
{/* Mobile cards */}
110+
<div className="md:hidden">
111+
{viewModels.length === 0 ? (
112+
<div className="rounded-md border border-border p-4 text-sm text-muted-foreground">
113+
No orders yet.
114+
</div>
115+
) : (
116+
<ul className="space-y-3">
117+
{viewModels.map(vm => (
118+
<li
119+
key={vm.id}
120+
className="rounded-lg border border-border bg-background p-4"
113121
>
114-
Total
115-
</th>
116-
<th
117-
scope="col"
118-
className="px-3 py-2 text-left font-semibold text-foreground"
119-
>
120-
Items
121-
</th>
122-
<th
123-
scope="col"
124-
className="px-3 py-2 text-left font-semibold text-foreground"
125-
>
126-
Provider
127-
</th>
128-
<th
129-
scope="col"
130-
className="px-3 py-2 text-left font-semibold text-foreground"
131-
>
132-
Order ID
133-
</th>
134-
<th
135-
scope="col"
136-
className="px-3 py-2 text-left font-semibold text-foreground"
137-
>
138-
Actions
139-
</th>
140-
</tr>
141-
</thead>
122+
<div className="flex items-start justify-between gap-3">
123+
<div className="min-w-0">
124+
<div className="text-xs text-muted-foreground">
125+
{vm.createdAt}
126+
</div>
127+
<div className="mt-1">
128+
<span className="inline-flex rounded-full bg-muted px-2 py-1 text-xs font-medium text-foreground">
129+
{vm.paymentStatus}
130+
</span>
131+
</div>
132+
</div>
133+
134+
<div className="shrink-0 whitespace-nowrap text-right text-sm font-medium text-foreground">
135+
{vm.totalFormatted}
136+
</div>
137+
</div>
138+
139+
<dl className="mt-3 grid grid-cols-2 gap-x-3 gap-y-2 text-xs">
140+
<div>
141+
<dt className="text-muted-foreground">Items</dt>
142+
<dd className="text-foreground">{vm.itemCount}</dd>
143+
</div>
142144

143-
<tbody className="divide-y divide-border">
144-
{items.length === 0 ? (
145+
<div className="min-w-0">
146+
<dt className="text-muted-foreground">Provider</dt>
147+
<dd
148+
className="truncate text-foreground"
149+
title={vm.paymentProvider}
150+
>
151+
{vm.paymentProvider}
152+
</dd>
153+
</div>
154+
155+
<div className="col-span-2">
156+
<dt className="text-muted-foreground">Order ID</dt>
157+
<dd
158+
className="break-all font-mono text-[11px] text-muted-foreground"
159+
title={vm.id}
160+
>
161+
{vm.id}
162+
</dd>
163+
</div>
164+
</dl>
165+
166+
<div className="mt-3">
167+
<Link
168+
href={vm.viewHref}
169+
className="inline-flex items-center justify-center rounded-md border border-border px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
170+
aria-label={vm.viewAriaLabel}
171+
>
172+
View
173+
</Link>
174+
</div>
175+
</li>
176+
))}
177+
</ul>
178+
)}
179+
</div>
180+
181+
{/* Desktop table */}
182+
<div className="hidden md:block">
183+
<div className="overflow-x-auto">
184+
<table className="min-w-full divide-y divide-border text-sm">
185+
<caption className="sr-only">Orders list</caption>
186+
187+
<thead className="bg-muted/50">
145188
<tr>
146-
<td className="px-3 py-6 text-muted-foreground" colSpan={7}>
147-
No orders yet.
148-
</td>
189+
<th
190+
scope="col"
191+
className="px-3 py-2 text-left font-semibold text-foreground"
192+
>
193+
Created
194+
</th>
195+
<th
196+
scope="col"
197+
className="px-3 py-2 text-left font-semibold text-foreground"
198+
>
199+
Status
200+
</th>
201+
<th
202+
scope="col"
203+
className="px-3 py-2 text-left font-semibold text-foreground"
204+
>
205+
Total
206+
</th>
207+
<th
208+
scope="col"
209+
className="px-3 py-2 text-left font-semibold text-foreground"
210+
>
211+
Items
212+
</th>
213+
<th
214+
scope="col"
215+
className="px-3 py-2 text-left font-semibold text-foreground"
216+
>
217+
Provider
218+
</th>
219+
<th
220+
scope="col"
221+
className="px-3 py-2 text-left font-semibold text-foreground"
222+
>
223+
Order ID
224+
</th>
225+
<th
226+
scope="col"
227+
className="px-3 py-2 text-left font-semibold text-foreground"
228+
>
229+
Actions
230+
</th>
149231
</tr>
150-
) : (
151-
items.map(order => {
152-
const currency = orderCurrency(order, locale);
153-
const totalMinor = pickMinor(
154-
order?.totalAmountMinor,
155-
order?.totalAmount
156-
);
157-
const totalFormatted =
158-
totalMinor === null
159-
? '-'
160-
: formatMoney(totalMinor, currency, locale);
161-
162-
return (
163-
<tr key={order.id} className="hover:bg-muted/50">
164-
<td className="px-3 py-2 text-muted-foreground">
165-
{formatDate(order.createdAt, locale)}
232+
</thead>
233+
234+
<tbody className="divide-y divide-border">
235+
{viewModels.length === 0 ? (
236+
<tr>
237+
<td
238+
className="px-3 py-6 text-muted-foreground"
239+
colSpan={7}
240+
>
241+
No orders yet.
242+
</td>
243+
</tr>
244+
) : (
245+
viewModels.map(vm => (
246+
<tr key={vm.id} className="hover:bg-muted/50">
247+
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">
248+
{vm.createdAt}
166249
</td>
167250

168-
<td className="px-3 py-2">
251+
<td className="px-3 py-2 whitespace-nowrap">
169252
<span className="inline-flex rounded-full bg-muted px-2 py-1 text-xs font-medium text-foreground">
170-
{order.paymentStatus}
253+
{vm.paymentStatus}
171254
</span>
172255
</td>
173256

174-
<td className="px-3 py-2 text-foreground">
175-
{totalFormatted}
257+
<td className="px-3 py-2 text-foreground whitespace-nowrap">
258+
{vm.totalFormatted}
176259
</td>
177260

178-
<td className="px-3 py-2 text-muted-foreground">
179-
{order.itemCount}
261+
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">
262+
{vm.itemCount}
180263
</td>
181-
<td className="px-3 py-2 text-muted-foreground">
182-
{order.paymentProvider}
264+
265+
<td className="px-3 py-2 text-muted-foreground whitespace-nowrap">
266+
{vm.paymentProvider}
183267
</td>
184268

185269
<td className="px-3 py-2 font-mono text-xs text-muted-foreground break-all">
186-
{order.id}
270+
{vm.id}
187271
</td>
188272

189-
<td className="px-3 py-2">
273+
<td className="px-3 py-2 whitespace-nowrap">
190274
<Link
191-
href={`/shop/admin/orders/${order.id}`}
275+
href={vm.viewHref}
192276
className="rounded-md border border-border px-2 py-1 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
193-
aria-label={`View order ${order.id}`}
277+
aria-label={vm.viewAriaLabel}
194278
>
195279
View
196280
</Link>
197281
</td>
198282
</tr>
199-
);
200-
})
201-
)}
202-
</tbody>
203-
</table>
283+
))
284+
)}
285+
</tbody>
286+
</table>
287+
</div>
204288
</div>
289+
205290
<div className="mt-4">
206291
<AdminPagination
207292
basePath="/shop/admin/orders"

0 commit comments

Comments
 (0)