Skip to content

Commit 847b6bb

Browse files
authored
Merge pull request #375 from firstJOASH/User-Management-Service
feat: user management system with profiles, preferences, auth, and an…
2 parents 4144510 + 2219573 commit 847b6bb

12 files changed

Lines changed: 804 additions & 17 deletions

File tree

app/src/App.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import { NavLink } from 'react-router-dom';
66
import Dashboard from './pages/Dashboard';
77
import TradeDetail from './pages/TradeDetail';
88
import CreateTrade from './pages/CreateTrade';
9-
import Help from './pages/Help';
9+
import Register from './pages/Register';
10+
import Login from './pages/Login';
11+
import UserProfile from './pages/UserProfile';
1012
import { ErrorBoundary } from './ErrorBoundary';
1113

1214
export default function App() {
@@ -19,19 +21,8 @@ export default function App() {
1921
<span className="nav-brand">StellarEscrow</span>
2022
<NavLink to="/" end>Dashboard</NavLink>
2123
<NavLink to="/trades/new">New Trade</NavLink>
22-
<NavLink to="/help">Help</NavLink>
23-
<button
24-
className="nav-mobile-toggle"
25-
onClick={() => setIsMenuOpen(!isMenuOpen)}
26-
aria-label="Toggle navigation"
27-
aria-expanded={isMenuOpen}
28-
>
29-
30-
</button>
31-
<div className={`nav-links ${isMenuOpen ? 'nav-links-open' : ''}`}>
32-
<NavLink to="/" end onClick={() => setIsMenuOpen(false)}>Dashboard</NavLink>
33-
<NavLink to="/trades/new" onClick={() => setIsMenuOpen(false)}>New Trade</NavLink>
34-
</div>
24+
<NavLink to="/login">Login</NavLink>
25+
<NavLink to="/register">Register</NavLink>
3526
</nav>
3627
<main className="main">
3728
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
@@ -54,7 +45,9 @@ export default function App() {
5445
<Route path="/" element={<Dashboard />} />
5546
<Route path="/trades/new" element={<CreateTrade />} />
5647
<Route path="/trades/:id" element={<TradeDetail />} />
57-
<Route path="/help" element={<Help />} />
48+
<Route path="/register" element={<Register />} />
49+
<Route path="/login" element={<Login />} />
50+
<Route path="/users/:address" element={<UserProfile />} />
5851
</Routes>
5952
</ErrorBoundary>
6053
</Container>

app/src/pages/Login.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useState } from 'react';
2+
import { useNavigate, Link } from 'react-router-dom';
3+
4+
const API = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
5+
6+
/**
7+
* Login: verifies the user exists on-chain by fetching their profile.
8+
* Real auth (wallet signing) would happen here; for now we just confirm
9+
* the address is registered and store it in sessionStorage.
10+
*/
11+
export default function Login() {
12+
const navigate = useNavigate();
13+
const [address, setAddress] = useState('');
14+
const [error, setError] = useState('');
15+
const [loading, setLoading] = useState(false);
16+
17+
async function handleSubmit(e: React.FormEvent) {
18+
e.preventDefault();
19+
setError('');
20+
setLoading(true);
21+
try {
22+
const res = await fetch(`${API}/users/${encodeURIComponent(address)}`);
23+
if (res.status === 404) throw new Error('Address not registered. Please register first.');
24+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
25+
sessionStorage.setItem('stellar_address', address);
26+
navigate(`/users/${address}`);
27+
} catch (err: any) {
28+
setError(err.message);
29+
} finally {
30+
setLoading(false);
31+
}
32+
}
33+
34+
return (
35+
<div style={styles.card}>
36+
<h2>Login</h2>
37+
<form onSubmit={handleSubmit} style={styles.form}>
38+
<label style={styles.label}>
39+
Stellar Address
40+
<input
41+
style={styles.input}
42+
value={address}
43+
onChange={(e) => setAddress(e.target.value)}
44+
placeholder="G…"
45+
required
46+
/>
47+
</label>
48+
{error && <p style={styles.error}>{error}</p>}
49+
<button style={styles.btn} type="submit" disabled={loading}>
50+
{loading ? 'Checking…' : 'Login'}
51+
</button>
52+
</form>
53+
<p style={{ marginTop: '1rem', fontSize: '0.875rem' }}>
54+
New user? <Link to="/register">Register</Link>
55+
</p>
56+
</div>
57+
);
58+
}
59+
60+
const styles: Record<string, React.CSSProperties> = {
61+
card: { maxWidth: 480, margin: '2rem auto', padding: '2rem', border: '1px solid #e2e8f0', borderRadius: 8 },
62+
form: { display: 'flex', flexDirection: 'column', gap: '1rem' },
63+
label: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: '0.875rem', fontWeight: 500 },
64+
input: { padding: '0.5rem', border: '1px solid #cbd5e0', borderRadius: 4, fontSize: '0.875rem' },
65+
btn: { padding: '0.6rem', background: '#1a1a2e', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' },
66+
error: { color: '#e53e3e', fontSize: '0.875rem' },
67+
};

app/src/pages/Register.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useState } from 'react';
2+
import { useNavigate, Link } from 'react-router-dom';
3+
4+
const API = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
5+
6+
export default function Register() {
7+
const navigate = useNavigate();
8+
const [address, setAddress] = useState('');
9+
const [usernameHash, setUsernameHash] = useState('');
10+
const [contactHash, setContactHash] = useState('');
11+
const [error, setError] = useState('');
12+
const [loading, setLoading] = useState(false);
13+
14+
async function handleSubmit(e: React.FormEvent) {
15+
e.preventDefault();
16+
setError('');
17+
setLoading(true);
18+
try {
19+
const res = await fetch(`${API}/users`, {
20+
method: 'POST',
21+
headers: { 'Content-Type': 'application/json' },
22+
body: JSON.stringify({
23+
address,
24+
username_hash: usernameHash,
25+
contact_hash: contactHash,
26+
}),
27+
});
28+
if (!res.ok) {
29+
const body = await res.json().catch(() => ({}));
30+
throw new Error(body?.error?.detail ?? `HTTP ${res.status}`);
31+
}
32+
navigate(`/users/${address}`);
33+
} catch (err: any) {
34+
setError(err.message);
35+
} finally {
36+
setLoading(false);
37+
}
38+
}
39+
40+
return (
41+
<div style={styles.card}>
42+
<h2>Register</h2>
43+
<p style={styles.hint}>
44+
Hashes are SHA-256 of the plaintext, computed client-side before submission.
45+
</p>
46+
<form onSubmit={handleSubmit} style={styles.form}>
47+
<label style={styles.label}>
48+
Stellar Address
49+
<input
50+
style={styles.input}
51+
value={address}
52+
onChange={(e) => setAddress(e.target.value)}
53+
placeholder="G…"
54+
required
55+
/>
56+
</label>
57+
<label style={styles.label}>
58+
Username Hash (SHA-256)
59+
<input
60+
style={styles.input}
61+
value={usernameHash}
62+
onChange={(e) => setUsernameHash(e.target.value)}
63+
placeholder="64-char hex"
64+
required
65+
/>
66+
</label>
67+
<label style={styles.label}>
68+
Contact Hash (SHA-256)
69+
<input
70+
style={styles.input}
71+
value={contactHash}
72+
onChange={(e) => setContactHash(e.target.value)}
73+
placeholder="64-char hex"
74+
required
75+
/>
76+
</label>
77+
{error && <p style={styles.error}>{error}</p>}
78+
<button style={styles.btn} type="submit" disabled={loading}>
79+
{loading ? 'Registering…' : 'Register'}
80+
</button>
81+
</form>
82+
<p style={{ marginTop: '1rem', fontSize: '0.875rem' }}>
83+
Already registered? <Link to="/login">Login</Link>
84+
</p>
85+
</div>
86+
);
87+
}
88+
89+
const styles: Record<string, React.CSSProperties> = {
90+
card: { maxWidth: 480, margin: '2rem auto', padding: '2rem', border: '1px solid #e2e8f0', borderRadius: 8 },
91+
hint: { fontSize: '0.8rem', color: '#666', marginBottom: '1rem' },
92+
form: { display: 'flex', flexDirection: 'column', gap: '1rem' },
93+
label: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: '0.875rem', fontWeight: 500 },
94+
input: { padding: '0.5rem', border: '1px solid #cbd5e0', borderRadius: 4, fontSize: '0.875rem' },
95+
btn: { padding: '0.6rem', background: '#1a1a2e', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' },
96+
error: { color: '#e53e3e', fontSize: '0.875rem' },
97+
};

0 commit comments

Comments
 (0)