Skip to content

Commit 20eb7b9

Browse files
authored
Merge pull request #1068 from emwulrd/feat/issues-706-707-710-713
Feat/issues 706 707 710 713
2 parents a61fc87 + a321475 commit 20eb7b9

4 files changed

Lines changed: 552 additions & 4095 deletions

File tree

Dechat/dex_with_fiat_frontend/src/components/AdminGuard.tsx

Lines changed: 46 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,206 +1,95 @@
11
'use client';
22

3-
import React, { useCallback, useEffect, useRef, useState } from 'react';
3+
import React, { useEffect, useState } from 'react';
44
import { z } from 'zod';
55
import { useStellarWallet } from '@/contexts/StellarWalletContext';
66
import { getAdmin } from '@/lib/stellarContract';
77
import LandingPage from '@/components/LandingPage';
88

9-
const stellarAddressSchema = z.string().length(56).startsWith('G');
9+
/** Zod schema for validating a Stellar public key (56-char G-prefixed string). */
10+
export const stellarAddressSchema = z.string().length(56).startsWith('G');
11+
12+
/** Inferred TypeScript type for a validated Stellar address. */
13+
export type StellarAddress = z.infer<typeof stellarAddressSchema>;
1014

1115
interface AdminGuardProps {
1216
children: React.ReactNode;
1317
}
1418

1519
/**
1620
* High-order component to guard admin routes.
17-
*
18-
* @param children - The components to render if authentication passes.
19-
*
20-
* @example
21-
* ```tsx
22-
* <AdminGuard>
23-
* <AdminDashboard />
24-
* </AdminGuard>
25-
* ```
26-
*
27-
* Architecture:
28-
* 1. **Session Check**: Verifies if a Stellar wallet is currently connected via `useStellarWallet`.
29-
* 2. **Blockchain Veracity**: Fetches the authorized admin address directly from the on-chain smart contract
30-
* using the `getAdmin()` helper. This bypasses local storage or session variables that could be tampered with.
31-
* 3. **Identity Comparison**: Compares the connected `G...` address against the contract's reported admin.
32-
* 4. **Offline Retry Queue**: When the network is unavailable the check is queued and automatically
33-
* retried as soon as the browser comes back online, so admins are never permanently locked out
34-
* by a transient connectivity loss.
35-
* 5. **Conditional Rendering**:
36-
* - If match: Renders `children`.
37-
* - If mismatch or no wallet: Redirects to `LandingPage`.
38-
* - If error: Displays a recovery UI with "Try Again" option.
39-
* - If offline with queued retry: Displays an offline banner.
40-
*
41-
* This implementation ensures that administrative privileges are strictly tied to the on-chain state,
42-
* providing a robust security layer against front-end spoofing.
21+
* Checks if the connected wallet address matches the admin address in the smart contract.
4322
*/
4423
export default function AdminGuard({ children }: AdminGuardProps) {
4524
const { connection } = useStellarWallet();
4625
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
4726
const [loading, setLoading] = useState(true);
4827
const [error, setError] = useState<string | null>(null);
49-
const [isOnline, setIsOnline] = useState(
50-
typeof navigator !== 'undefined' ? navigator.onLine : true,
51-
);
52-
const [retryQueued, setRetryQueued] = useState(false);
53-
54-
// Stable ref so the online handler can call the latest checkAdmin without a
55-
// stale closure, even if connection.address changes between renders.
56-
const checkAdminRef = useRef<(() => Promise<void>) | undefined>(undefined);
57-
58-
const checkAdmin = useCallback(async () => {
59-
if (!navigator.onLine) {
60-
setRetryQueued(true);
61-
setLoading(false);
62-
return;
63-
}
6428

65-
setRetryQueued(false);
66-
setLoading(true);
67-
setError(null);
68-
69-
if (!connection.address) {
70-
setIsAdmin(false);
71-
setLoading(false);
72-
return;
73-
}
74-
75-
const connectedParsed = stellarAddressSchema.safeParse(connection.address);
76-
if (!connectedParsed.success) {
77-
console.error('Invalid connected wallet address format:', connectedParsed.error);
78-
setError('Invalid wallet address format. Access denied.');
79-
setIsAdmin(false);
80-
setLoading(false);
81-
return;
82-
}
29+
useEffect(() => {
30+
async function checkAdmin() {
31+
if (!connection.address) {
32+
setIsAdmin(false);
33+
setLoading(false);
34+
return;
35+
}
8336

84-
try {
85-
const adminAddress = await getAdmin();
86-
const adminParsed = stellarAddressSchema.safeParse(adminAddress);
87-
if (!adminParsed.success) {
88-
console.error('Invalid admin address configured in contract:', adminParsed.error);
89-
setError('Invalid contract configuration. Access denied.');
37+
const connectedParsed = stellarAddressSchema.safeParse(connection.address);
38+
if (!connectedParsed.success) {
39+
console.error('Invalid connected wallet address format:', connectedParsed.error);
40+
setError('Invalid wallet address format. Access denied.');
9041
setIsAdmin(false);
42+
setLoading(false);
9143
return;
9244
}
9345

94-
setIsAdmin(connectedParsed.data === adminParsed.data);
95-
} catch (err) {
96-
console.error('Failed to verify admin status:', err);
97-
setError('Failed to verify admin status. Please try again.');
98-
setIsAdmin(false);
99-
} finally {
100-
setLoading(false);
46+
try {
47+
const adminAddress = await getAdmin();
48+
const adminParsed = stellarAddressSchema.safeParse(adminAddress);
49+
if (!adminParsed.success) {
50+
console.error('Invalid admin address configured in contract:', adminParsed.error);
51+
setError('Invalid contract configuration. Access denied.');
52+
setIsAdmin(false);
53+
return;
54+
}
55+
56+
setIsAdmin(connectedParsed.data === adminParsed.data);
57+
} catch (err) {
58+
console.error('Failed to verify admin status:', err);
59+
setError('Failed to verify admin status. Please try again.');
60+
setIsAdmin(false);
61+
} finally {
62+
setLoading(false);
63+
}
10164
}
102-
}, [connection.address]);
103-
104-
// Keep the ref in sync so the online handler always calls the latest version.
105-
checkAdminRef.current = checkAdmin;
10665

107-
// Run admin check whenever the connected address changes.
108-
useEffect(() => {
10966
checkAdmin();
110-
}, [checkAdmin]);
111-
112-
// #490: Scroll to top smoothly whenever admin access is granted so the
113-
// dashboard renders from the top of the page, not wherever the user
114-
// navigated from. Only fires when loading completes and access is confirmed.
115-
useEffect(() => {
116-
if (!loading && isAdmin) {
117-
window.scrollTo({ top: 0, behavior: 'smooth' });
118-
}
119-
}, [loading, isAdmin]);
120-
121-
// Attach online/offline listeners once.
122-
useEffect(() => {
123-
const handleOnline = () => {
124-
setIsOnline(true);
125-
// Flush the queue: retry the admin check that was skipped while offline.
126-
checkAdminRef.current?.();
127-
};
128-
const handleOffline = () => {
129-
setIsOnline(false);
130-
};
131-
132-
window.addEventListener('online', handleOnline);
133-
window.addEventListener('offline', handleOffline);
134-
return () => {
135-
window.removeEventListener('online', handleOnline);
136-
window.removeEventListener('offline', handleOffline);
137-
};
138-
}, []);
139-
140-
if (retryQueued && !isOnline) {
141-
return (
142-
<div className="flex h-screen flex-col items-center justify-center bg-[var(--color-surface)] p-6 text-center">
143-
<svg
144-
className="mx-auto mb-4 h-12 w-12"
145-
style={{ color: 'var(--color-text-muted)' }}
146-
fill="none"
147-
viewBox="0 0 24 24"
148-
stroke="currentColor"
149-
aria-hidden="true"
150-
>
151-
<path
152-
strokeLinecap="round"
153-
strokeLinejoin="round"
154-
strokeWidth={2}
155-
d="M18.364 5.636a9 9 0 010 12.728M15.536 8.464a5 5 0 010 7.072M6.343 6.343a9 9 0 000 12.728m2.829-2.829a5 5 0 000-7.07"
156-
/>
157-
</svg>
158-
<h2
159-
className="mb-2 text-xl font-bold"
160-
style={{ color: 'var(--color-text-primary)' }}
161-
>
162-
You are offline
163-
</h2>
164-
<p
165-
className="text-sm"
166-
style={{ color: 'var(--color-text-muted)' }}
167-
>
168-
Admin verification will retry automatically when your connection is restored.
169-
</p>
170-
</div>
171-
);
172-
}
67+
}, [connection.address]);
17368

17469
if (loading) {
17570
return (
176-
<div className="flex h-screen items-center justify-center bg-[var(--color-surface)] text-[var(--color-text-primary)]">
71+
<div className="theme-app flex h-screen items-center justify-center">
17772
<div
17873
className="h-8 w-8 animate-spin rounded-full border-4"
17974
style={{
180-
borderColor: 'var(--color-primary)',
181-
borderTopColor: 'transparent',
75+
borderColor: 'var(--color-border)',
76+
borderTopColor: 'var(--color-primary)',
18277
}}
183-
></div>
184-
<span className="ml-3 font-medium" style={{ color: 'var(--color-text-primary)' }}>
185-
Verifying admin access...
186-
</span>
78+
/>
79+
<span className="theme-text-secondary ml-3 font-medium">Verifying admin access...</span>
18780
</div>
18881
);
18982
}
19083

19184
if (error) {
19285
return (
193-
<div
194-
className="flex h-screen flex-col items-center justify-center bg-[var(--color-surface)] text-[var(--color-text-primary)] p-6 text-center"
195-
>
86+
<div className="theme-app flex h-screen flex-col items-center justify-center p-6 text-center">
19687
<div className="mb-4" style={{ color: 'var(--color-danger)' }}>
197-
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
88+
<svg className="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19889
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
19990
</svg>
20091
</div>
201-
<h2 className="mb-2 text-xl font-bold" style={{ color: 'var(--color-text-primary)' }}>
202-
{error}
203-
</h2>
92+
<h2 className="theme-text-primary text-xl font-bold mb-2">{error}</h2>
20493
<button
20594
onClick={() => window.location.reload()}
20695
className="theme-primary-button rounded-lg px-4 py-2 text-sm font-medium"

0 commit comments

Comments
 (0)