Skip to content

Commit e6b6179

Browse files
authored
fix: batch resolve security + bug issues #55-#74 (#75)
* fix: batch resolve security + bug issues #55-#74 VERIFY issues: - #55: Signup already correctly calls /api/auth/register + redirects to /dashboard (verified OK) - #56/#65: Set metadataBase to https://coinpayportal.com, replaced all localhost:8080 fallbacks in WalletContext, SDK, and docs - #57: Enhanced signup form with email format validation, password strength (8+ chars, uppercase, lowercase, number), inline field errors, disabled submit while invalid - #58: Escrow page count now shows recurring series alongside one-time escrows; empty state already checks both - #59: Header already uses isHydrated guard with loading placeholder (verified OK) SECURITY issues: - #62: Replaced CORS wildcard with strict origin whitelist (coinpayportal.com, www.coinpayportal.com) in proxy.ts - #63/#71: Added rate limiting to proxy.ts (60 req/min general, 10 req/min auth) with X-RateLimit-* headers - #64: Admin page now returns notFound() - #66: Wallet creation rate limiting already working via checkRateLimitAsync (verified OK) - #67: Wallet signature auth already has replay protection via checkAndRecordSignature + ±5min timestamp window (verified OK) - #69: Removed unsafe-eval from CSP in proxy.ts; unsafe-inline documented as required by Next.js - #70: Replaced specific RPC URLs in CSP connect-src with generic https: wss: patterns - #72: /api/reputation root endpoint now returns 404 instead of listing all endpoints - #73: Webhook receiver returns generic 401 instead of revealing config status - #74: Added *.log, strix_runs/, strix_*.log to .gitignore; removed tracked debug files Skipped: #38 (invoicing), #60 (multi-sig) — too large No SKILL.md found for #68 Note: 1 pre-existing test failure in businesses/[id]/page.test.tsx (unrelated to these changes) * fix: address Copilot review comments on PR #75
1 parent 988ab60 commit e6b6179

File tree

11 files changed

+219
-80
lines changed

11 files changed

+219
-80
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ node_modules
2424
npm-debug.log*
2525
yarn-debug.log*
2626
yarn-error.log*
27+
*.log
28+
29+
# strix agent runs
30+
strix_runs/
31+
strix_*.log
2732

2833
# local env files
2934
.env*.local

src/app/admin/page.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1+
import { notFound } from 'next/navigation';
2+
13
export default function AdminPage() {
2-
return (
3-
<div className="container mx-auto px-4 py-16">
4-
<h1 className="text-4xl font-bold mb-8">Admin Panel</h1>
5-
<p className="text-gray-600">Admin dashboard coming soon...</p>
6-
</div>
7-
);
8-
}
4+
notFound();
5+
}

src/app/api/reputation/route.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,8 @@
11
import { NextResponse } from 'next/server';
22

33
export async function GET() {
4-
return NextResponse.json({
5-
service: 'reputation',
6-
endpoints: [
7-
'/api/reputation/agent/:did/reputation',
8-
'/api/reputation/badge/:did',
9-
'/api/reputation/check',
10-
'/api/reputation/trust',
11-
'/api/reputation/attest',
12-
'/api/reputation/credential/:id',
13-
'/api/reputation/credentials',
14-
'/api/reputation/did/register',
15-
'/api/reputation/did/claim',
16-
'/api/reputation/did/me',
17-
'/api/reputation/issuers',
18-
'/api/reputation/receipt',
19-
'/api/reputation/receipts',
20-
'/api/reputation/verify',
21-
'/api/reputation/platform-action',
22-
'/api/reputation/revocation-list',
23-
],
24-
});
4+
return NextResponse.json(
5+
{ success: false, error: 'Not found' },
6+
{ status: 404 }
7+
);
258
}

src/app/api/webhook-receiver/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export async function POST(request: NextRequest) {
7676
if (!webhookSecret) {
7777
console.error('WEBHOOK_SECRET environment variable is not set');
7878
return NextResponse.json(
79-
{ success: false, error: 'Webhook receiver not configured' },
79+
{ error: 'Internal server error' },
8080
{ status: 500 }
8181
);
8282
}

src/app/escrow/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ export default function EscrowDashboardPage() {
370370
))}
371371
<p className="text-center text-sm text-gray-400 mt-4">
372372
Showing {escrows.length} of {total} escrows
373+
{series.length > 0 && ` + ${series.length} recurring series`}
373374
</p>
374375
</div>
375376

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Providers } from '@/components/Providers';
66
import './globals.css';
77

88
export const metadata: Metadata = {
9-
metadataBase: new URL('https://coinpayportal.com'),
9+
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'https://coinpayportal.com'),
1010
title: 'CoinPay - Non-Custodial Crypto Payment Gateway',
1111
description: 'Accept cryptocurrency payments in your e-commerce store with automatic fee handling and real-time processing',
1212
keywords: ['cryptocurrency', 'payment gateway', 'crypto payments', 'non-custodial', 'blockchain', 'bitcoin', 'ethereum'],

src/app/signup/page.tsx

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,51 @@ export default function SignupPage() {
1313
name: '',
1414
});
1515
const [error, setError] = useState('');
16+
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
1617
const [loading, setLoading] = useState(false);
1718

19+
const validateEmail = (email: string): string | null => {
20+
if (!email) return 'Email is required';
21+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Invalid email format';
22+
return null;
23+
};
24+
25+
const validatePassword = (password: string): string | null => {
26+
if (!password) return 'Password is required';
27+
if (password.length < 8) return 'Must be at least 8 characters';
28+
if (!/[A-Z]/.test(password)) return 'Must contain an uppercase letter';
29+
if (!/[a-z]/.test(password)) return 'Must contain a lowercase letter';
30+
if (!/[0-9]/.test(password)) return 'Must contain a number';
31+
return null;
32+
};
33+
34+
const isFormValid = (): boolean => {
35+
return (
36+
!validateEmail(formData.email) &&
37+
!validatePassword(formData.password) &&
38+
formData.password === formData.confirmPassword &&
39+
formData.password.length > 0
40+
);
41+
};
42+
1843
const handleSubmit = async (e: React.FormEvent) => {
1944
e.preventDefault();
2045
setError('');
2146

22-
// Validate passwords match
47+
// Inline validation
48+
const errors: Record<string, string> = {};
49+
const emailErr = validateEmail(formData.email);
50+
if (emailErr) errors.email = emailErr;
51+
const passErr = validatePassword(formData.password);
52+
if (passErr) errors.password = passErr;
2353
if (formData.password !== formData.confirmPassword) {
24-
setError('Passwords do not match');
25-
return;
54+
errors.confirmPassword = 'Passwords do not match';
2655
}
27-
28-
// Validate password strength
29-
if (formData.password.length < 8) {
30-
setError('Password must be at least 8 characters');
56+
if (Object.keys(errors).length > 0) {
57+
setFieldErrors(errors);
3158
return;
3259
}
60+
setFieldErrors({});
3361

3462
setLoading(true);
3563

@@ -118,12 +146,16 @@ export default function SignupPage() {
118146
type="email"
119147
required
120148
value={formData.email}
121-
onChange={(e) =>
122-
setFormData({ ...formData, email: e.target.value })
123-
}
124-
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900"
149+
onChange={(e) => {
150+
setFormData({ ...formData, email: e.target.value });
151+
if (fieldErrors.email) setFieldErrors((prev) => ({ ...prev, email: '' }));
152+
}}
153+
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900 ${fieldErrors.email ? 'border-red-400' : 'border-gray-300'}`}
125154
placeholder="you@example.com"
126155
/>
156+
{fieldErrors.email && (
157+
<p className="mt-1 text-xs text-red-600">{fieldErrors.email}</p>
158+
)}
127159
</div>
128160

129161
<div>
@@ -138,12 +170,16 @@ export default function SignupPage() {
138170
type="password"
139171
required
140172
value={formData.password}
141-
onChange={(e) =>
142-
setFormData({ ...formData, password: e.target.value })
143-
}
144-
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900"
173+
onChange={(e) => {
174+
setFormData({ ...formData, password: e.target.value });
175+
if (fieldErrors.password) setFieldErrors((prev) => ({ ...prev, password: '' }));
176+
}}
177+
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900 ${fieldErrors.password ? 'border-red-400' : 'border-gray-300'}`}
145178
placeholder="••••••••"
146179
/>
180+
{fieldErrors.password && (
181+
<p className="mt-1 text-xs text-red-600">{fieldErrors.password}</p>
182+
)}
147183
<p className="mt-1 text-xs text-gray-500">
148184
Must be at least 8 characters with uppercase, lowercase, and numbers
149185
</p>
@@ -161,17 +197,21 @@ export default function SignupPage() {
161197
type="password"
162198
required
163199
value={formData.confirmPassword}
164-
onChange={(e) =>
165-
setFormData({ ...formData, confirmPassword: e.target.value })
166-
}
167-
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900"
200+
onChange={(e) => {
201+
setFormData({ ...formData, confirmPassword: e.target.value });
202+
if (fieldErrors.confirmPassword) setFieldErrors((prev) => ({ ...prev, confirmPassword: '' }));
203+
}}
204+
className={`w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder:text-gray-400 text-gray-900 ${fieldErrors.confirmPassword ? 'border-red-400' : 'border-gray-300'}`}
168205
placeholder="••••••••"
169206
/>
207+
{fieldErrors.confirmPassword && (
208+
<p className="mt-1 text-xs text-red-600">{fieldErrors.confirmPassword}</p>
209+
)}
170210
</div>
171211

172212
<button
173213
type="submit"
174-
disabled={loading}
214+
disabled={loading || !isFormValid()}
175215
className="w-full bg-purple-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
176216
>
177217
{loading ? 'Creating account...' : 'Sign up'}

src/components/docs/WebWalletDocs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ pnpm coinpay-wallet create --chains BTC,ETH,SOL`}
9393
<div className="mb-8 p-4 bg-slate-800/50 rounded-lg">
9494
<h4 className="text-white font-semibold text-sm mb-2">Environment Variables</h4>
9595
<div className="space-y-1 text-sm text-gray-300">
96-
<p><code className="text-purple-400">COINPAY_API_URL</code> — API base URL (default: <code>http://localhost:8080</code>)</p>
96+
<p><code className="text-purple-400">NEXT_PUBLIC_API_URL</code> — API base URL (default: <code>https://coinpayportal.com</code>). The legacy <code>COINPAY_API_URL</code> is also supported by the CLI.</p>
9797
<p><code className="text-purple-400">COINPAY_AUTH_TOKEN</code> — JWT token for read-only operations</p>
9898
<p><code className="text-purple-400">COINPAY_MNEMONIC</code> — Mnemonic phrase (required for <code>send</code> and <code>derive-missing</code>)</p>
9999
</div>

src/components/web-wallet/WalletContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function getBaseUrl(): string {
102102
if (typeof window !== 'undefined') {
103103
return window.location.origin;
104104
}
105-
return process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:8080';
105+
return process.env.NEXT_PUBLIC_APP_URL || (process.env.NODE_ENV === 'production' ? 'https://coinpayportal.com' : 'http://localhost:3000');
106106
}
107107

108108
export function WebWalletProvider({ children }: { children: ReactNode }) {

src/lib/sdk/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function createCoinPayClient(
9494
): InstanceType<typeof SDKCoinPayClient> {
9595
return new SDKCoinPayClient({
9696
apiKey,
97-
baseUrl: baseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
97+
baseUrl: baseUrl || process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'production' ? 'https://coinpayportal.com' : 'http://localhost:8080'),
9898
});
9999
}
100100

0 commit comments

Comments
 (0)