diff --git a/server.ts b/server.ts
index b4bf9bbf..5c2c6aa7 100644
--- a/server.ts
+++ b/server.ts
@@ -15,7 +15,7 @@ import * as db from './src/services/db';
import { logger } from './src/services/logger'; // This import is correct
import * as aiService from './src/services/aiService.server'; // Import the new server-side AI service
import { sendPasswordResetEmail } from './src/services/emailService';
-import { Profile, UserProfile, ShoppingListItem, ReceiptItem } from './src/types';
+import { UserProfile, ShoppingListItem } from './src/types';
// Load environment variables from a .env file at the root of your project
dotenv.config();
@@ -122,7 +122,7 @@ passport.use(new LocalStrategy(
}
// 3. Success! Return the user object (without password_hash for security).
- const { password_hash, ...userWithoutHash } = user;
+ const { password_hash: _password_hash, ...userWithoutHash } = user;
logger.info(`User successfully authenticated: ${email}`);
return done(null, userWithoutHash);
} catch (err) {
@@ -958,8 +958,8 @@ app.post('/api/auth/register', async (req: Request, res: Response, next: NextFun
// Login Route
app.post('/api/auth/login', (req: Request, res: Response, next: NextFunction) => {
// Use passport.authenticate with the 'local' strategy
- // { session: false } because we're using JWTs, not server-side sessions
- passport.authenticate('local', { session: false }, (err: Error, user: Express.User | false, info: { message: string }) => {
+ // { session: false } because we're using JWTs, not server-side sessions. The 'info' object is not used, so it's removed.
+ passport.authenticate('local', { session: false }, (err: Error, user: Express.User | false) => {
const { rememberMe } = req.body; // Get the 'rememberMe' flag from the request
if (err) {
logger.error('Login authentication error in /login route:', { error: err });
@@ -967,7 +967,7 @@ app.post('/api/auth/login', (req: Request, res: Response, next: NextFunction) =>
}
if (!user) {
// Authentication failed (e.g., incorrect credentials)
- return res.status(401).json({ message: info ? info.message : 'Login failed' });
+ return res.status(401).json({ message: 'Login failed' });
}
// User is authenticated, create and sign a JWT
@@ -1587,7 +1587,7 @@ app.get('/api/receipts/:id/deals', passport.authenticate('jwt', { session: false
// --- Error Handling and Server Startup ---
// Basic error handling middleware
-app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
+app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
logger.error('Unhandled application error:', { error: err.stack });
res.status(500).send('Something broke!');
});
diff --git a/src/App.tsx b/src/App.tsx
index 405638e7..d2f84771 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,7 +10,7 @@ import { ErrorDisplay } from './components/ErrorDisplay';
import { Header } from './components/Header';
import { logger } from './services/logger'; // This is correct
import { isImageAFlyer, extractCoreDataFromImage, extractAddressFromImage, extractLogoFromImage } from './services/aiApiClient'; // prettier-ignore
-import type { FlyerItem, Flyer, MasterGroceryItem, DealItem, ProcessingStage, StageStatus, Profile, ShoppingList, ShoppingListItem, User, UserProfile } from './types';
+import type { FlyerItem, Flyer, MasterGroceryItem, DealItem, ProcessingStage, StageStatus, Profile, ShoppingList, ShoppingListItem, User } from './types';
import { BulkImporter } from './components/BulkImporter';
import { PriceHistoryChart } from './components/PriceHistoryChart'; // This import seems to have a supabase dependency, but the component is not provided. Assuming it will be updated separately.
import { getAuthenticatedUserProfile, fetchFlyers as apiFetchFlyers, fetchMasterItems as apiFetchMasterItems, fetchWatchedItems as apiFetchWatchedItems, addWatchedItem as apiAddWatchedItem, removeWatchedItem as apiRemoveWatchedItem, fetchShoppingLists as apiFetchShoppingLists, createShoppingList as apiCreateShoppingList, deleteShoppingList as apiDeleteShoppingList, addShoppingListItem as apiAddShoppingListItem, updateShoppingListItem as apiUpdateShoppingListItem, removeShoppingListItem as apiRemoveShoppingListItem, processFlyerFile, fetchFlyerItems as apiFetchFlyerItems, fetchFlyerItemsForFlyers as apiFetchFlyerItemsForFlyers, countFlyerItemsForFlyers as apiCountFlyerItemsForFlyers, uploadLogoAndUpdateStore } from './services/apiClient'; // updateUserPreferences is no longer called directly from App.tsx
@@ -32,6 +32,7 @@ import { WatchedItemsList } from './components/WatchedItemsList';
import { AdminStatsPage } from './pages/AdminStatPages';
import { ResetPasswordPage } from './pages/ResetPasswordPage';
import { AnonymousUserBanner } from './components/AnonymousUserBanner';
+import { VoiceLabPage } from './pages/VoiceLabPage'; // Import the new page
/**
* Defines the possible authentication states for a user session.
@@ -133,10 +134,9 @@ function App() {
try {
// Fetch all essential user data *before* setting the final authenticated state.
// This ensures the app doesn't enter an inconsistent state if one of these calls fails.
- const [userProfile, watchedData, listsData] = await Promise.all([
+ const [userProfile, watchedData] = await Promise.all([
getAuthenticatedUserProfile(),
apiFetchWatchedItems(),
- apiFetchShoppingLists(),
]);
// Now that all data is successfully fetched, update the application state.
@@ -144,10 +144,9 @@ function App() {
setUser(loggedInUser); // Or userProfile.user, which should be identical
setAuthStatus('AUTHENTICATED');
setWatchedItems(watchedData);
- setShoppingLists(listsData);
- if (listsData.length > 0) {
- setActiveListId(listsData[0].id);
- }
+
+ // The fetchShoppingLists function will be triggered by the useEffect below
+ // now that the user state has been set.
logger.info('Login and data fetch successful', { user: loggedInUser });
} catch (e) {
@@ -184,7 +183,7 @@ function App() {
const errorMessage = e instanceof Error ? e.message : String(e);
setError(`Could not fetch watched items: ${errorMessage}`);
}
- }, []); // No dependencies on user or profile, as this is general data
+ }, [user]);
const fetchShoppingLists = useCallback(async () => {
if (!user) { // Check for authenticated user
@@ -204,7 +203,7 @@ function App() {
const errorMessage = e instanceof Error ? e.message : String(e);
setError(`Could not fetch shopping lists: ${errorMessage}`);
}
- }, [activeListId]); // activeListId is a dependency for managing the active list
+ }, [user, activeListId]); // user is a dependency to ensure we fetch lists for the correct user.
const fetchMasterItems = useCallback(async () => {
try {
@@ -229,6 +228,11 @@ function App() {
setUser(userProfile.user);
setProfile(userProfile);
setAuthStatus('AUTHENTICATED');
+
+ // Fetch user-specific data now that authentication is confirmed.
+ // These functions are safe to call here because they check for the user internally.
+ fetchWatchedItems();
+ fetchShoppingLists();
logger.info('Token validated successfully.', { user: userProfile.user });
} catch (e) {
logger.warn('Auth token validation failed. Clearing token.', { error: e });
@@ -242,15 +246,22 @@ function App() {
}
};
checkAuthToken();
- }, []);
+ }, [fetchWatchedItems, fetchShoppingLists]); // Add callbacks to dependency array.
useEffect(() => {
if (isReady) {
fetchFlyers();
fetchMasterItems();
+
+ // If the user is already authenticated when the app becomes ready,
+ // fetch their specific data.
+ if (authStatus === 'AUTHENTICATED') {
+ fetchWatchedItems();
+ fetchShoppingLists();
+ }
}
- }, [isReady, fetchFlyers, fetchMasterItems]);
+ }, [isReady, authStatus, fetchFlyers, fetchMasterItems, fetchWatchedItems, fetchShoppingLists]);
const resetState = useCallback(() => {
@@ -883,6 +894,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 8cc11800..7386e267 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React from 'react';
import { ShoppingCartIcon } from './icons/ShoppingCartIcon';
import { UserIcon } from './icons/UserIcon';
import { Cog8ToothIcon } from './icons/Cog8ToothIcon';
diff --git a/src/components/PasswordInput.tsx b/src/components/PasswordInput.tsx
new file mode 100644
index 00000000..50f26c27
--- /dev/null
+++ b/src/components/PasswordInput.tsx
@@ -0,0 +1,46 @@
+import React, { useState } from 'react';
+import { EyeIcon } from './icons/EyeIcon';
+import { EyeSlashIcon } from './icons/EyeSlashIcon';
+import { PasswordStrengthIndicator } from './PasswordStrengthIndicator';
+
+/**
+ * Props for the PasswordInput component.
+ * It extends standard HTML input attributes and adds a custom prop to show a strength indicator.
+ */
+interface PasswordInputProps extends React.InputHTMLAttributes {
+ showStrength?: boolean;
+}
+
+/**
+ * A reusable password input component with a show/hide toggle
+ * and an optional password strength indicator.
+ */
+export const PasswordInput: React.FC = ({ showStrength = false, className, ...props }) => {
+ const [showPassword, setShowPassword] = useState(false);
+
+ return (
+