OK
This commit is contained in:
39
frontend/.dockerignore
Normal file
39
frontend/.dockerignore
Normal 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
|
||||
@@ -28,6 +28,7 @@ const PrivateRoute = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// 需要認證才能進入應用
|
||||
return user ? <Layout><Outlet /></Layout> : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 === '';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default defineConfig({
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:12000',
|
||||
target: 'http://backend:5000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user