Full analytics platform with MaxMind GeoLite2 IP-to-location resolution, cross-module dashboard (docs, video, photo), user drill-down, volunteer self-service stats, and ANALYTICS_ADMIN role with feature flag controls. - ANALYTICS_ADMIN role + ANALYTICS_ROLES group across backend and frontend - GeoIP service (MaxMind GeoLite2, lazy-loaded, graceful degradation) - Geo fields (country, region, city, lat/lng) on DocsPageView, VideoView, PhotoView - IP resolved to geo before SHA-256 hashing (privacy-preserving) - Unified analytics module: overview, geo, content, user engagement endpoints - 4 admin dashboard pages: Overview, Geography (Leaflet map), Content, Users - Volunteer MyAnalyticsPage for self-service activity stats - Settings UI: enableAnalytics, analyticsGeoEnabled, trackAuthenticatedUsers, retentionDays - Scheduled cleanup job respecting configurable retention period - config.sh: Analytics + MaxMind prompt in configure_features() - Control panel: enableAnalytics flag, template, discovery, wizard, detail page - Docker: geoip volume mount, MaxMind env vars, entrypoint auto-download - Nginx: X-Forwarded-For fix ($proxy_add_x_forwarded_for) for real client IP - Express trust proxy set to 2 for Pangolin/Newt tunnel chain - CORS updated for docs origin (cmlite.org + docs.cmlite.org) - Lander page: added docs-analytics tracking snippet - Prisma migration: 20260402100000_add_analytics_system Bunker Admin
75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
import { UserRole } from '@prisma/client';
|
|
|
|
const ROLE_PRIORITY: Record<string, number> = {
|
|
SUPER_ADMIN: 5,
|
|
INFLUENCE_ADMIN: 4,
|
|
MAP_ADMIN: 4,
|
|
BROADCAST_ADMIN: 4,
|
|
CONTENT_ADMIN: 4,
|
|
MEDIA_ADMIN: 4,
|
|
PAYMENTS_ADMIN: 4,
|
|
EVENTS_ADMIN: 4,
|
|
SOCIAL_ADMIN: 4,
|
|
POLLS_ADMIN: 4,
|
|
ANALYTICS_ADMIN: 4,
|
|
USER: 2,
|
|
TEMP: 1,
|
|
};
|
|
|
|
/** All admin roles (any user with one of these can access /app) */
|
|
export const ADMIN_ROLES: UserRole[] = [
|
|
UserRole.SUPER_ADMIN,
|
|
UserRole.INFLUENCE_ADMIN,
|
|
UserRole.MAP_ADMIN,
|
|
UserRole.BROADCAST_ADMIN,
|
|
UserRole.CONTENT_ADMIN,
|
|
UserRole.MEDIA_ADMIN,
|
|
UserRole.PAYMENTS_ADMIN,
|
|
UserRole.EVENTS_ADMIN,
|
|
UserRole.SOCIAL_ADMIN,
|
|
UserRole.POLLS_ADMIN,
|
|
UserRole.ANALYTICS_ADMIN,
|
|
];
|
|
|
|
// Module-specific role groups
|
|
export const INFLUENCE_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN];
|
|
export const MAP_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN];
|
|
export const BROADCAST_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.BROADCAST_ADMIN];
|
|
export const CONTENT_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.CONTENT_ADMIN];
|
|
export const MEDIA_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MEDIA_ADMIN];
|
|
export const PAYMENTS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.PAYMENTS_ADMIN];
|
|
export const EVENTS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.EVENTS_ADMIN];
|
|
export const SOCIAL_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.SOCIAL_ADMIN];
|
|
export const SYSTEM_ROLES: UserRole[] = [UserRole.SUPER_ADMIN];
|
|
export const SCHEDULING_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.MAP_ADMIN, UserRole.EVENTS_ADMIN];
|
|
export const POLLS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.POLLS_ADMIN, UserRole.INFLUENCE_ADMIN];
|
|
export const ANALYTICS_ROLES: UserRole[] = [UserRole.SUPER_ADMIN, UserRole.ANALYTICS_ADMIN];
|
|
|
|
/** Check if the user has any of the specified roles */
|
|
export function hasAnyRole(user: { roles?: unknown; role?: UserRole }, roles: UserRole[]): boolean {
|
|
const userRoles = getUserRoles(user);
|
|
return userRoles.some(r => roles.includes(r));
|
|
}
|
|
|
|
/** Check if user has any admin role */
|
|
export function isAdmin(user: { roles?: unknown; role?: UserRole }): boolean {
|
|
return hasAnyRole(user, ADMIN_ROLES);
|
|
}
|
|
|
|
/** Get the primary (highest-priority) role from a roles array */
|
|
export function getPrimaryRole(roles: UserRole[]): UserRole {
|
|
if (roles.length === 0) return UserRole.USER;
|
|
return roles.reduce((highest, current) =>
|
|
(ROLE_PRIORITY[current] || 0) > (ROLE_PRIORITY[highest] || 0) ? current : highest
|
|
);
|
|
}
|
|
|
|
/** Safely extract UserRole[] from a user object (handles old single-role and new multi-role) */
|
|
export function getUserRoles(user: { roles?: unknown; role?: UserRole }): UserRole[] {
|
|
if (Array.isArray(user.roles) && user.roles.length > 0) {
|
|
return user.roles as UserRole[];
|
|
}
|
|
if (user.role) return [user.role];
|
|
return [UserRole.USER];
|
|
}
|