feat: add admin user system with role-based access control

Implement comprehensive admin user system for Kubernetes deployment:

Backend:
- Add isAdmin field to User model for role-based permissions
- Create adminAuth middleware to protect admin-only routes
- Protect 11 routes across rewards, cache, streets, and analytics endpoints
- Update setup-couchdb.js to seed default admin user at deployment

Kubernetes:
- Add ADMIN_EMAIL and ADMIN_PASSWORD to secrets.yaml
- Add ADMIN_EMAIL to configmap.yaml for non-sensitive config
- Create couchdb-init-job.yaml for automated database initialization
- Update secrets.yaml.example with admin user documentation

Frontend:
- Create AdminRoute component for admin-only page protection
- Create comprehensive AdminDashboard with 5 tabs:
  * Overview: Platform statistics and quick actions
  * Users: List, search, manage admin status, delete users
  * Streets: Create, edit, delete streets
  * Rewards: Create, edit, toggle, delete rewards
  * Content: Moderate posts and events
- Add Admin navigation link in Navbar (visible only to admins)
- Integrate admin routes in App.js

Default admin user:
- Email: will@wills-portal.com
- Created automatically by K8s init job at deployment

Routes protected:
- POST/PUT/DELETE /api/rewards (catalog management)
- POST /api/streets (street creation)
- DELETE /api/cache (cache operations)
- GET /api/analytics/* (platform statistics)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
William Valentin
2025-12-06 13:36:15 -08:00
parent 71c1d82e0e
commit fc23f4d098
15 changed files with 1159 additions and 22 deletions
+5
View File
@@ -1,5 +1,6 @@
const express = require("express");
const auth = require("../middleware/auth");
const adminAuth = require("../middleware/adminAuth");
const { asyncHandler } = require("../middleware/errorHandler");
const { getCacheMiddleware, invalidateCacheByPattern } = require("../middleware/cache");
const couchdbService = require("../services/couchdbService");
@@ -77,6 +78,7 @@ const groupByTimePeriod = (data, groupBy = "day", dateField = "createdAt") => {
router.get(
"/overview",
auth,
adminAuth,
getCacheMiddleware(300), // Cache for 5 minutes
asyncHandler(async (req, res) => {
const { timeframe = "all" } = req.query;
@@ -249,6 +251,7 @@ router.get(
router.get(
"/activity",
auth,
adminAuth,
getCacheMiddleware(300), // Cache for 5 minutes
asyncHandler(async (req, res) => {
const { timeframe = "30d", groupBy = "day" } = req.query;
@@ -335,6 +338,7 @@ router.get(
router.get(
"/top-contributors",
auth,
adminAuth,
getCacheMiddleware(300), // Cache for 5 minutes
asyncHandler(async (req, res) => {
const { limit = 10, timeframe = "all", metric = "points" } = req.query;
@@ -472,6 +476,7 @@ router.get(
router.get(
"/street-stats",
auth,
adminAuth,
getCacheMiddleware(300), // Cache for 5 minutes
asyncHandler(async (req, res) => {
const { timeframe = "all" } = req.query;
+2
View File
@@ -1,5 +1,6 @@
const express = require("express");
const auth = require("../middleware/auth");
const adminAuth = require("../middleware/adminAuth");
const { asyncHandler } = require("../middleware/errorHandler");
const { getCacheStats, clearCache } = require("../middleware/cache");
@@ -31,6 +32,7 @@ router.get(
router.delete(
"/",
auth,
adminAuth,
asyncHandler(async (req, res) => {
clearCache();
res.json({
+6
View File
@@ -1,6 +1,7 @@
const express = require("express");
const Reward = require("../models/Reward");
const auth = require("../middleware/auth");
const adminAuth = require("../middleware/adminAuth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
createRewardValidation,
@@ -28,6 +29,7 @@ router.get(
router.post(
"/",
auth,
adminAuth,
createRewardValidation,
asyncHandler(async (req, res) => {
const { name, description, cost, isPremium } = req.body;
@@ -102,6 +104,7 @@ router.get(
router.put(
"/:id",
auth,
adminAuth,
rewardIdValidation,
createRewardValidation,
asyncHandler(async (req, res) => {
@@ -126,6 +129,7 @@ router.put(
router.delete(
"/:id",
auth,
adminAuth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const reward = await Reward.findById(req.params.id);
@@ -229,6 +233,7 @@ router.get(
router.patch(
"/:id/toggle",
auth,
adminAuth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const updatedReward = await Reward.toggleActiveStatus(req.params.id);
@@ -240,6 +245,7 @@ router.patch(
router.post(
"/bulk",
auth,
adminAuth,
asyncHandler(async (req, res) => {
const { rewards } = req.body;
+2
View File
@@ -3,6 +3,7 @@ const Street = require("../models/Street");
const User = require("../models/User");
const couchdbService = require("../services/couchdbService");
const auth = require("../middleware/auth");
const adminAuth = require("../middleware/adminAuth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
createStreetValidation,
@@ -103,6 +104,7 @@ router.get(
router.post(
"/",
auth,
adminAuth,
createStreetValidation,
asyncHandler(async (req, res) => {
const { name, location } = req.body;