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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user