Initial commit: Trading Portal - FastAPI + React + PostgreSQL

This commit is contained in:
2026-07-03 13:08:09 +00:00
commit 34a1e91541
198 changed files with 35110 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
import { useAppDispatch } from '../../app/hooks';
import { loginThunk } from './authSlice';
import { useState, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useT } from '../../translations';
export default function LoginPage() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { t } = useT();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); // P2-12
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
setIsSubmitting(true); // P2-12: show loading state
try {
await dispatch(loginThunk({ username, password })).unwrap();
navigate('/dashboard');
} catch (err) {
setError(typeof err === 'string' ? err : t('login.error'));
} finally {
setIsSubmitting(false);
}
};
return (
<div className="auth-page">
<div className="auth-card" style={{ maxWidth: '100vw', boxSizing: 'border-box' }}>
<h1>{t('login.title')}</h1>
<h2>{t('login.subtitle')}</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">{t('login.username')}</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t('login.username')}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">{t('login.password')}</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t('login.password')}
required
/>
</div>
<button type="submit" className="btn btn-primary btn-full" disabled={isSubmitting} style={{ minHeight: 44, touchAction: 'manipulation' }}>
{isSubmitting ? t('login.signing') : t('login.submit')}
</button>
</form>
{/* Register temporarily hidden while features stabilize */}
{/* <p className="auth-link"> */}
{/* Don't have an account? <a href="/register">Register</a> */}
{/* </p> */}
</div>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
import { useAppDispatch } from '../../app/hooks';
import { registerThunk } from './authSlice';
import { useState, type FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
export default function RegisterPage() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
try {
await dispatch(registerThunk({ username, email, password })).unwrap();
navigate('/dashboard');
} catch (err) {
setError(typeof err === 'string' ? err : 'Registration failed');
}
};
return (
<div className="auth-page">
<div className="auth-card" style={{ maxWidth: '100vw', boxSizing: 'border-box' }}>
<h1>Trading Portal</h1>
<h2>Create Account</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Choose a username"
required
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Create a password"
required
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm your password"
required
/>
</div>
<button type="submit" className="btn btn-primary btn-full" style={{ minHeight: 44, touchAction: 'manipulation' }}>
Create Account
</button>
</form>
<p className="auth-link">
Already have an account? <a href="/login">Sign In</a>
</p>
</div>
</div>
);
}
+160
View File
@@ -0,0 +1,160 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { AuthState, User } from '../../types/trading';
import * as api from '../api/apiService';
function loadInitialState(): AuthState {
const token = localStorage.getItem('access_token');
return {
user: null,
accessToken: token,
refreshToken: localStorage.getItem('refresh_token'),
isAuthenticated: !!token,
isLoading: false,
error: null,
};
}
export const loginThunk = createAsyncThunk(
'auth/login',
async ({ username, password }: { username: string; password: string }, { rejectWithValue }) => {
try {
const result = await api.login(username, password);
localStorage.setItem('access_token', result.access_token);
localStorage.setItem('refresh_token', result.refresh_token);
// Fetch user info after login
const user = await api.getMe();
return { user, accessToken: result.access_token, refreshToken: result.refresh_token };
} catch (err: unknown) {
if (err instanceof Error) {
return rejectWithValue(err.message);
}
return rejectWithValue('Login failed');
}
},
);
export const registerThunk = createAsyncThunk(
'auth/register',
async (
{ username, email, password }: { username: string; email: string; password: string },
{ rejectWithValue },
) => {
try {
await api.register(username, email, password);
// Auto-login after registration
const result = await api.login(username, password);
localStorage.setItem('access_token', result.access_token);
localStorage.setItem('refresh_token', result.refresh_token);
const user = await api.getMe();
return { user, accessToken: result.access_token, refreshToken: result.refresh_token };
} catch (err: unknown) {
if (err instanceof Error) {
return rejectWithValue(err.message);
}
return rejectWithValue('Registration failed');
}
},
);
export const fetchCurrentUser = createAsyncThunk(
'auth/fetchCurrentUser',
async (_, { rejectWithValue }) => {
try {
const user = await api.getMe();
return user as User;
} catch (err: unknown) {
if (err instanceof Error) {
return rejectWithValue(err.message);
}
return rejectWithValue('Failed to fetch user');
}
},
);
export const logoutThunk = createAsyncThunk(
'auth/logout',
async () => {
await api.logout();
},
);
const authSlice = createSlice({
name: 'auth',
initialState: loadInitialState(),
reducers: {
setCredentials(state, action: PayloadAction<{ user: User; accessToken: string; refreshToken: string }>) {
state.user = action.payload.user;
state.accessToken = action.payload.accessToken;
state.refreshToken = action.payload.refreshToken;
state.isAuthenticated = true;
state.error = null;
},
clearCredentials(state) {
state.user = null;
state.accessToken = null;
state.refreshToken = null;
state.isAuthenticated = false;
state.error = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
},
clearError(state) {
state.error = null;
},
},
extraReducers: (builder) => {
builder
// Login
.addCase(loginThunk.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(loginThunk.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
state.accessToken = action.payload.accessToken;
state.refreshToken = action.payload.refreshToken;
state.isAuthenticated = true;
})
.addCase(loginThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Login failed';
})
// Register
.addCase(registerThunk.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(registerThunk.fulfilled, (state) => {
state.isLoading = false;
})
.addCase(registerThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = (action.payload as string) || 'Registration failed';
})
// Fetch current user
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
state.user = action.payload;
state.isAuthenticated = true;
})
.addCase(fetchCurrentUser.rejected, (state) => {
state.user = null;
state.accessToken = null;
state.refreshToken = null;
state.isAuthenticated = false;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
})
// Logout
.addCase(logoutThunk.fulfilled, (state) => {
state.user = null;
state.accessToken = null;
state.refreshToken = null;
state.isAuthenticated = false;
});
},
});
export const { setCredentials, clearCredentials, clearError } = authSlice.actions;
export default authSlice.reducer;