feat: Responsive admin UI, SEO pages, and click analytics

## Responsive Admin UI
- Layout.tsx: Mobile sidebar drawer with hamburger menu
- Dashboard.tsx: 2-col grid on mobile, responsive stats cards
- OrchestratorDashboard.tsx: Responsive table with hidden columns
- PagesTab.tsx: Responsive filters and table

## SEO Pages
- New /admin/seo section with state landing pages
- SEO page generation and management
- State page content with dispensary/product counts

## Click Analytics
- Product click tracking infrastructure
- Click analytics dashboard

## Other Changes
- Consumer features scaffolding (alerts, deals, favorites)
- Health panel component
- Workers dashboard improvements
- Legacy DutchieAZ pages removed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-07 22:48:21 -07:00
parent 38d3ea1408
commit 3bc0effa33
74 changed files with 12295 additions and 807 deletions

View File

@@ -140,11 +140,72 @@ export function requireRole(...roles: string[]) {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
/**
* Optional auth middleware - attempts to authenticate but allows unauthenticated requests
*
* If a valid token is provided, sets req.user with the authenticated user.
* If no token or invalid token, continues without setting req.user.
*
* Use this for endpoints that work for both authenticated and anonymous users
* (e.g., product click tracking where we want user_id when available).
*/
export async function optionalAuthMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
// No token provided - continue without auth
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return next();
}
const token = authHeader.substring(7);
// Try JWT first
const jwtUser = verifyToken(token);
if (jwtUser) {
req.user = jwtUser;
return next();
}
// If JWT fails, try API token
try {
const result = await pool.query(`
SELECT id, name, rate_limit, active, expires_at
FROM api_tokens
WHERE token = $1
`, [token]);
if (result.rows.length > 0) {
const apiToken = result.rows[0];
// Check if token is active and not expired
if (apiToken.active && (!apiToken.expires_at || new Date(apiToken.expires_at) >= new Date())) {
req.apiToken = {
id: apiToken.id,
name: apiToken.name,
rate_limit: apiToken.rate_limit
};
req.user = {
id: apiToken.id,
email: `api-token-${apiToken.id}@system`,
role: 'api'
};
}
}
} catch (error) {
// Silently ignore errors - optional auth should not fail the request
console.warn('[OptionalAuth] Error checking API token:', error);
}
next();
}