This commit is contained in:
beabigegg
2025-11-13 08:18:15 +08:00
parent 788e2409df
commit df5411e44c
38 changed files with 1163 additions and 445 deletions

39
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,39 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Production build
dist
build
# Environment files
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# Logs
logs
*.log
# Coverage
coverage
# Misc
.npmrc

View File

@@ -28,6 +28,7 @@ const PrivateRoute = () => {
);
}
// 需要認證才能進入應用
return user ? <Layout><Outlet /></Layout> : <Navigate to="/login" />;
};

View File

@@ -14,50 +14,50 @@ const setAuthToken = token => {
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(() => localStorage.getItem('token'));
const [token, setToken] = useState(localStorage.getItem('token'));
const [loading, setLoading] = useState(true);
useEffect(() => {
if (token) {
try {
const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000;
if (decoded.exp < currentTime) {
console.log("Token expired, logging out.");
logout();
} else {
setUser({
id: decoded.sub,
role: decoded.role,
username: decoded.username
});
setAuthToken(token);
// Check if token exists and validate it
const validateToken = async () => {
const savedToken = localStorage.getItem('token');
if (savedToken) {
try {
setAuthToken(savedToken);
const response = await axios.get('http://localhost:5000/api/me');
setUser(response.data);
setToken(savedToken);
} catch (error) {
console.error('Token validation failed:', error);
localStorage.removeItem('token');
setToken(null);
setUser(null);
setAuthToken(null);
}
} catch (error) {
console.error("Invalid token on initial load");
logout();
}
}
setLoading(false);
}, [token]);
setLoading(false);
};
validateToken();
}, []);
const login = async (username, password) => {
try {
const response = await axios.post('/api/login', { username, password });
const { access_token } = response.data;
const response = await axios.post('http://localhost:5000/api/login', { username, password });
const { access_token, user: userData } = response.data;
localStorage.setItem('token', access_token);
setToken(access_token);
const decoded = jwtDecode(access_token);
setUser({
id: decoded.sub,
role: decoded.role,
username: decoded.username
});
setUser(userData);
setAuthToken(access_token);
return { success: true };
return { success: true, user: userData };
} catch (error) {
console.error('Login failed:', error.response?.data?.msg || error.message);
return { success: false, message: error.response?.data?.msg || 'Login failed' };
console.error('Login failed:', error.response?.data?.error || error.message);
return {
success: false,
message: error.response?.data?.error || 'Login failed'
};
}
};
@@ -78,7 +78,13 @@ export const AuthProvider = ({ children }) => {
return (
<AuthContext.Provider value={value}>
{!loading && children}
{loading ? (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
) : (
children
)}
</AuthContext.Provider>
);
};

View File

@@ -45,9 +45,10 @@ const DashboardPage = () => {
const fetchMeetings = useCallback(async () => {
try {
const data = await getMeetings();
setMeetings(data);
setMeetings(Array.isArray(data) ? data : []);
} catch (err) {
setError('Could not fetch meetings.');
setMeetings([]); // 確保設置為空陣列
} finally {
setLoading(false);
}
@@ -101,11 +102,14 @@ const DashboardPage = () => {
};
const uniqueStatuses = useMemo(() => {
if (!Array.isArray(meetings)) return [];
const statuses = new Set(meetings.map(m => m.status));
return Array.from(statuses);
}, [meetings]);
const filteredAndSortedMeetings = useMemo(() => {
if (!Array.isArray(meetings)) return [];
let filtered = meetings.filter(meeting => {
const topicMatch = meeting.topic.toLowerCase().includes(topicSearch.toLowerCase());
const ownerMatch = meeting.owner_name ? meeting.owner_name.toLowerCase().includes(ownerSearch.toLowerCase()) : ownerSearch === '';

View File

@@ -10,11 +10,8 @@ import { register } from '../services/api';
const LoginPage = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [loading, setLoading] = useState(false);
const [isRegister, setIsRegister] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
@@ -25,7 +22,6 @@ const LoginPage = () => {
const handleLogin = async (e) => {
e.preventDefault();
setError('');
setSuccess('');
setLoading(true);
const { success, message } = await login(username, password);
if (success) {
@@ -36,27 +32,6 @@ const LoginPage = () => {
setLoading(false);
};
const handleRegister = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
setError('Passwords do not match.');
return;
}
setError('');
setSuccess('');
setLoading(true);
try {
await register(username, password);
setSuccess('Account created successfully! Please log in.');
setIsRegister(false); // Switch back to login view
setUsername(''); // Clear fields
setPassword('');
setConfirmPassword('');
} catch (err) {
setError(err.response?.data?.error || 'Failed to create account.');
}
setLoading(false);
};
return (
<Container component="main" maxWidth="xs">
@@ -81,15 +56,15 @@ const LoginPage = () => {
AI Meeting Assistant
</Typography>
<Typography component="h2" variant="subtitle1" sx={{ mt: 1 }}>
{isRegister ? 'Create Account' : 'Sign In'}
使用 AD 帳號登入
</Typography>
<Box component="form" onSubmit={isRegister ? handleRegister : handleLogin} noValidate sx={{ mt: 1 }}>
<Box component="form" onSubmit={handleLogin} noValidate sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="username"
label="Username"
label="AD 帳號 (例如: username@panjit.com.tw)"
name="username"
autoComplete="username"
autoFocus
@@ -101,29 +76,14 @@ const LoginPage = () => {
required
fullWidth
name="password"
label="Password"
label="AD 密碼"
type="password"
id="password"
autoComplete={isRegister ? "new-password" : "current-password"}
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{isRegister && (
<TextField
margin="normal"
required
fullWidth
name="confirmPassword"
label="Confirm Password"
type="password"
id="confirmPassword"
autoComplete="new-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
)}
{error && <Alert severity="error" sx={{ width: '100%', mt: 2 }}>{error}</Alert>}
{success && <Alert severity="success" sx={{ width: '100%', mt: 2 }}>{success}</Alert>}
<Button
type="submit"
fullWidth
@@ -131,19 +91,8 @@ const LoginPage = () => {
sx={{ mt: 3, mb: 2 }}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : (isRegister ? 'Create Account' : 'Sign In')}
{loading ? <CircularProgress size={24} /> : '登入'}
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
<Link href="#" variant="body2" onClick={() => {
setIsRegister(!isRegister);
setError('');
setSuccess('');
}}>
{isRegister ? "Already have an account? Sign In" : "Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>

View File

@@ -7,7 +7,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:12000',
target: 'http://backend:5000',
changeOrigin: true,
},
},