Skip to content

Commit 5e0d984

Browse files
authored
Merge pull request #167 from devOgazi/feat/add-input-sanitization
Feat/add input sanitization
2 parents af384e2 + bcbf33c commit 5e0d984

6 files changed

Lines changed: 70 additions & 15 deletions

File tree

app/(merchant)/payments/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
DialogFooter
1919
} from '@/components/ui/dialog';
2020
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
21+
import { trimInput } from '@/lib/utils/sanitize';
2122
import { useNotify } from '@/lib/hooks/useNotify';
2223

2324
interface PaymentLink {
@@ -76,7 +77,8 @@ export default function PaymentsPage() {
7677

7778
const handleCreate = (e: React.FormEvent) => {
7879
e.preventDefault();
79-
if (!labelValue.trim()) {
80+
const sanitizedLabel = trimInput(labelValue);
81+
if (!sanitizedLabel) {
8082
setLabelError('Label is required');
8183
return;
8284
}

app/(merchant)/settings/page.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Settings, User, Building2, Bell, Shield, LogOut } from 'lucide-react';
1010
import { useAuthStore } from '@/lib/store/authStore';
1111
import { useRouter } from 'next/navigation';
1212
import { useNotify } from '@/lib/hooks/useNotify';
13+
import { trimInput, normalizeEmail } from '@/lib/utils/sanitize';
1314
import { cn } from '@/lib/utils';
1415

1516
const tabs = [
@@ -38,6 +39,19 @@ export default function SettingsPage() {
3839
const { user, logout } = useAuthStore();
3940
const notify = useNotify();
4041

42+
const [profileName, setProfileName] = useState(user?.name ?? '');
43+
const [profileEmail, setProfileEmail] = useState(user?.email ?? '');
44+
const [profilePhone, setProfilePhone] = useState('');
45+
46+
const [bizName, setBizName] = useState('Merchant Corp');
47+
const [bizRegNumber, setBizRegNumber] = useState('');
48+
const [bizBankName, setBizBankName] = useState('');
49+
const [bizAccountNumber, setBizAccountNumber] = useState('');
50+
51+
const [currentPassword, setCurrentPassword] = useState('');
52+
const [newPassword, setNewPassword] = useState('');
53+
const [confirmNewPassword, setConfirmNewPassword] = useState('');
54+
4155
const handleLogout = useCallback(() => {
4256
logout();
4357
notify.success('Logged out successfully');
@@ -115,20 +129,25 @@ export default function SettingsPage() {
115129
<div className="grid gap-4 sm:grid-cols-2">
116130
<div className="space-y-1.5">
117131
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Full Name</Label>
118-
<Input defaultValue={user?.name ?? ''} className="h-10 border-border rounded-xl bg-card text-sm" />
132+
<Input value={profileName} onChange={(e) => setProfileName(e.target.value)} className="h-10 border-border rounded-xl bg-card text-sm" />
119133
</div>
120134
<div className="space-y-1.5">
121135
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Email Address</Label>
122-
<Input defaultValue={user?.email ?? ''} type="email" className="h-10 border-border rounded-xl bg-card text-sm" />
136+
<Input value={profileEmail} onChange={(e) => setProfileEmail(e.target.value)} type="email" className="h-10 border-border rounded-xl bg-card text-sm" />
123137
</div>
124138
</div>
125139
<div className="space-y-1.5">
126140
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Phone Number</Label>
127-
<Input placeholder="+234 800 000 0000" className="h-10 border-border rounded-xl bg-card text-sm" />
141+
<Input value={profilePhone} onChange={(e) => setProfilePhone(e.target.value)} placeholder="+234 800 000 0000" className="h-10 border-border rounded-xl bg-card text-sm" />
128142
</div>
129143
<Button
130144
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold rounded-xl h-10 px-6 text-sm scroll-mb-52"
131145
onClick={() => {
146+
const sanitized = {
147+
name: trimInput(profileName),
148+
email: normalizeEmail(profileEmail),
149+
phone: trimInput(profilePhone),
150+
};
132151
notify.success('Profile updated');
133152
}}
134153
>
@@ -147,24 +166,30 @@ export default function SettingsPage() {
147166
<div className="grid gap-4 sm:grid-cols-2">
148167
<div className="space-y-1.5">
149168
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Business Name</Label>
150-
<Input defaultValue="Merchant Corp" className="h-10 border-border rounded-xl bg-card text-sm" />
169+
<Input value={bizName} onChange={(e) => setBizName(e.target.value)} className="h-10 border-border rounded-xl bg-card text-sm" />
151170
</div>
152171
<div className="space-y-1.5">
153172
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Registration Number</Label>
154-
<Input placeholder="RC-1234567" className="h-10 border-border rounded-xl bg-card text-sm" />
173+
<Input value={bizRegNumber} onChange={(e) => setBizRegNumber(e.target.value)} placeholder="RC-1234567" className="h-10 border-border rounded-xl bg-card text-sm" />
155174
</div>
156175
<div className="space-y-1.5">
157176
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Bank Name</Label>
158-
<Input placeholder="e.g. GTBank" className="h-10 border-border rounded-xl bg-card text-sm" />
177+
<Input value={bizBankName} onChange={(e) => setBizBankName(e.target.value)} placeholder="e.g. GTBank" className="h-10 border-border rounded-xl bg-card text-sm" />
159178
</div>
160179
<div className="space-y-1.5">
161180
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Account Number</Label>
162-
<Input placeholder="0123456789" className="h-10 border-border rounded-xl bg-card text-sm" />
181+
<Input value={bizAccountNumber} onChange={(e) => setBizAccountNumber(e.target.value)} placeholder="0123456789" className="h-10 border-border rounded-xl bg-card text-sm" />
163182
</div>
164183
</div>
165184
<Button
166185
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold rounded-xl h-10 px-6 text-sm scroll-mb-52"
167186
onClick={() => {
187+
const sanitized = {
188+
name: trimInput(bizName),
189+
regNumber: trimInput(bizRegNumber),
190+
bankName: trimInput(bizBankName),
191+
accountNumber: trimInput(bizAccountNumber),
192+
};
168193
notify.success('Business info saved');
169194
}}
170195
>
@@ -205,19 +230,24 @@ export default function SettingsPage() {
205230
<CardContent className="space-y-5">
206231
<div className="space-y-1.5">
207232
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Current Password</Label>
208-
<Input type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
233+
<Input value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
209234
</div>
210235
<div className="space-y-1.5">
211236
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">New Password</Label>
212-
<Input type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
237+
<Input value={newPassword} onChange={(e) => setNewPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
213238
</div>
214239
<div className="space-y-1.5">
215240
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Confirm New Password</Label>
216-
<Input type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
241+
<Input value={confirmNewPassword} onChange={(e) => setConfirmNewPassword(e.target.value)} type="password" placeholder="••••••••" className="h-10 border-border rounded-xl bg-card text-sm" />
217242
</div>
218243
<Button
219244
className="bg-primary hover:bg-primary/90 text-primary-foreground font-semibold rounded-xl h-10 px-6 text-sm scroll-mb-52"
220245
onClick={() => {
246+
const sanitized = {
247+
currentPassword: trimInput(currentPassword),
248+
newPassword: trimInput(newPassword),
249+
confirmNewPassword: trimInput(confirmNewPassword),
250+
};
221251
notify.success('Password updated');
222252
}}
223253
>

app/(merchant)/transactions/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CurrencyDisplay } from '@/components/shared/CurrencyDisplay';
1313
import { EmptyState } from '@/components/shared/EmptyState';
1414
import { mockTransactions } from '@/lib/mock/transactions';
1515
import { formatDate } from '@/lib/utils/format';
16+
import { sanitizeSearchQuery } from '@/lib/utils/sanitize';
1617
import { Search, Download, Filter, SearchX } from 'lucide-react';
1718
import { TransactionDetail } from '@/components/transactions/TransactionDetail';
1819
import { Transaction } from '@/lib/mock/transactions';
@@ -97,6 +98,7 @@ const TransactionCard = memo(function TransactionCard({ tx, onClick }: Transacti
9798

9899
export default function TransactionsPage() {
99100
const [searchTerm, setSearchTerm] = useState('');
101+
const sanitizedOnChange = (value: string) => setSearchTerm(sanitizeSearchQuery(value));
100102
const debouncedSearch = useDebounceValue(searchTerm, 300);
101103
const [filterCount] = useState(0);
102104
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
@@ -131,7 +133,7 @@ export default function TransactionsPage() {
131133
placeholder="Search by hash or address..."
132134
className="w-full pl-9 bg-background/50 border-border/50 focus-visible:ring-ring"
133135
value={searchTerm}
134-
onChange={(e) => setSearchTerm(e.target.value)}
136+
onChange={(e) => sanitizedOnChange(e.target.value)}
135137
/>
136138
</div>
137139
<div className="flex gap-2">

app/auth/login/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useNotify } from '@/lib/hooks/useNotify';
1111
import dynamic from 'next/dynamic';
1212

1313
import { loginSchema, LoginFormValues } from '@/lib/utils/validation';
14+
import { normalizeEmail } from '@/lib/utils/sanitize';
1415
import { useAuthStore } from '@/lib/store/authStore';
1516
import { useRateLimitStore } from '@/lib/store/rateLimitStore';
1617
import { Button } from '@/components/ui/button';
@@ -51,8 +52,9 @@ export default function LoginPage() {
5152

5253
const onSubmit = useCallback(async (data: LoginFormValues) => {
5354
setIsLoading(true);
55+
const sanitizedData = { ...data, email: normalizeEmail(data.email) };
5456
try {
55-
const isMockAdmin = data.email.includes('admin');
57+
const isMockAdmin = sanitizedData.email.includes('admin');
5658
const role = isMockAdmin ? 'admin' : 'merchant';
5759

5860
let merchantId = 'GCCHHKNI7GRA5QWC7RCTT3OHO7SKAUMKQA6IBWEQEO2SXI3GF376UHDD';
@@ -73,7 +75,7 @@ export default function LoginPage() {
7375
const mockToken = 'mock_jwt_token_12345';
7476
const mockUser = {
7577
id: merchantId,
76-
email: data.email,
78+
email: sanitizedData.email,
7779
name: merchantName,
7880
role,
7981
};

app/auth/register/page.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Loader2, Check } from 'lucide-react';
1010
import { useNotify } from '@/lib/hooks/useNotify';
1111

1212
import { registerSchema, RegisterFormValues, passwordRequirements } from '@/lib/utils/validation';
13+
import { trimInput, normalizeEmail } from '@/lib/utils/sanitize';
1314
import { useWalletStore } from '@/lib/store/walletStore';
1415
import { Button } from '@/components/ui/button';
1516
import { Input } from '@/components/ui/input';
@@ -55,12 +56,17 @@ export default function RegisterPage() {
5556

5657
const onSubmit = async (data: RegisterFormValues) => {
5758
setIsLoading(true);
59+
const sanitizedData = {
60+
...data,
61+
businessName: trimInput(data.businessName),
62+
email: normalizeEmail(data.email),
63+
};
5864
try {
5965
try {
6066
const { apiClient } = await import('@/lib/api/axios');
6167
await apiClient.post('/api/merchants', {
6268
id: `merch_${Math.random().toString(36).substr(2, 9)}`,
63-
name: data.businessName,
69+
name: sanitizedData.businessName,
6470
});
6571
} catch {
6672
console.warn('Backend unavailable, falling back to mock registration for Vercel preview.');

lib/utils/sanitize.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const SEARCH_QUERY_MAX_LENGTH = 100;
2+
3+
export function trimInput(value: string): string {
4+
return value.trim();
5+
}
6+
7+
export function normalizeEmail(email: string): string {
8+
return email.trim().toLowerCase();
9+
}
10+
11+
export function sanitizeSearchQuery(query: string): string {
12+
return query.trim().slice(0, SEARCH_QUERY_MAX_LENGTH);
13+
}

0 commit comments

Comments
 (0)