Fix: ManageUsers component missing useAuth() - caused blank admin page
The ManageUsers sub-component referenced user?.role for conditional role dropdown options but didn't call useAuth() to get the user object. This caused 'user is not defined' JS errors and blank admin pages. Browser E2E: 20/20 tests pass.
This commit is contained in:
@@ -9,6 +9,7 @@ datasource db {
|
|||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
employee
|
employee
|
||||||
|
office
|
||||||
admin
|
admin
|
||||||
super_admin
|
super_admin
|
||||||
}
|
}
|
||||||
|
|||||||
+140
-45
@@ -4,52 +4,147 @@ const bcrypt = require('bcryptjs');
|
|||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
const DEFAULT_HOMEOWNERS = [
|
const DEFAULT_HOMEOWNERS = [
|
||||||
'Anderson, 217',
|
'Freeman, 101',
|
||||||
'Bakos',
|
'Williamson (Clam Shell Cottage), 102',
|
||||||
'Beckstead, 111',
|
'Best, Steve (Hog Heaven), 103',
|
||||||
'Bentley, 310',
|
'Kuchman, Jeff & Cindy (Easter Cottage), 104',
|
||||||
'Best, 103',
|
'Parsons, David & Donna (Ibis Cottage), 105A',
|
||||||
'Caraway, 132',
|
'Carroll / Young (Egret Cottage), 105B',
|
||||||
'Carmichael, M, 216',
|
'Chapin, Kay & Charlie (Morning Glory), 106',
|
||||||
'Casa Blanca',
|
'Young (Sanibel Cottage), 107',
|
||||||
'Chapin, 106',
|
'Hager A (Banyan Cottage), 108',
|
||||||
'Conner, 309',
|
'Coyle (Cayo Costa), 109',
|
||||||
'Cook, 118',
|
'Bound, Simon (Mondongo), 110',
|
||||||
'Coyle, 109',
|
'Beckstead (A Gasparilla, B Sunset I), 111AB',
|
||||||
'Davis, 114a',
|
'Symonds (Captiva Cottage), 112',
|
||||||
'Dimmitt, 213',
|
'Kerr (Shell Cottage), 113',
|
||||||
'Dockery, 502',
|
'Davis, Beth (White Sands Cottage), 114A',
|
||||||
'Fassett, 303C',
|
'UIDC (Turtle Grass Cottage), 114B',
|
||||||
'Gypsy Wind',
|
'Fletemeyer, John, 115',
|
||||||
'Hager, 108',
|
'Kimberg (Cottage 16 North), 116A',
|
||||||
'Hanford, 308',
|
'Corey (Bamboo Cottage), 116B',
|
||||||
'Hitchcox – Clarry, 218',
|
'Wilson, Clyde (High Tide), 117',
|
||||||
'Hughes, 215',
|
'Cook, Randy & Beata (Cottage 18), 118',
|
||||||
'Kaufman 129 (Blue View)',
|
'Miller (Folly Cottage), 119',
|
||||||
'Kuchman, 104',
|
'Miller (Honeymoon Cottage), 120',
|
||||||
'Lockhart, 301A',
|
'Miller (Conch Out Cottage), 121',
|
||||||
'Lokey, 136',
|
'Miller - gazebo, 122',
|
||||||
'McColgan, 312',
|
'Simpson (North Point Cottage), 123A',
|
||||||
'Mercurio, 523',
|
'Amsler, Virginia (Ginny) (Whelk Cottage), 123B',
|
||||||
'Moff – Dean Elect',
|
'Prosser (Sea Mystic), 124',
|
||||||
'Rogers, 501',
|
'Strickland, Bonnie, 125',
|
||||||
'Rusten, 204A',
|
'Beisswenger/ Kruzi, 126',
|
||||||
'Ryan, 301B',
|
'Albert, Michael (Cottage 27), 127',
|
||||||
'Salas, 144',
|
'Sear (Twin Shore), 128',
|
||||||
'Sear 128 (Twin Shores)',
|
'Kaufman, Steve (Blue View), 129',
|
||||||
'Shimp, 517',
|
'Fetter, Tim & Megan, 130A',
|
||||||
'Sipprelle, 202',
|
'Ullrich, David & Anja, 130B',
|
||||||
'Trino, 131',
|
'Trino, Vic & Suzy, 131',
|
||||||
'Useppa Fire',
|
'Caraway, Jim (Dolphin Cottage), 132',
|
||||||
'Vogt',
|
'Escobar, Rick (Little Mermaid), 133',
|
||||||
'Weinsz, 141',
|
'Vance, Paul (Serne Mermaid), 134',
|
||||||
'Wendorf, 306',
|
'Lokey (lot), 135',
|
||||||
'White (Rogan)',
|
'Lokey, Phil & Shannon, 136',
|
||||||
'Williams, Bob, 140',
|
'Wright, Clark (Fish Hawk), 137A',
|
||||||
'Williams, Dan, 137B',
|
'Williams, Dan, 137B',
|
||||||
'Williamson-Whetstone, 102',
|
'Preckwinkle, George, 138A',
|
||||||
'Wilson, George, 516',
|
'Preckwinkle, George (Calusa South), 138B',
|
||||||
'Wright, 137A',
|
'Collier - lot, 139',
|
||||||
|
'Williams, Bob, 140',
|
||||||
|
'Weinsz, Steve, 141',
|
||||||
|
'Lawler, Michael (Sea level), 142A',
|
||||||
|
'Lawler (Sea Level Cottage), 142B',
|
||||||
|
'Ketay, 143',
|
||||||
|
'Salas, Joe (West Wind), 144',
|
||||||
|
'Buchanan (Eagles Nest), 145',
|
||||||
|
'Buchanan (Eagles Nest), 146',
|
||||||
|
'Nutting - lot, 147',
|
||||||
|
'Firehouse, 200',
|
||||||
|
'Smith, Sawyer (Alibi Cottage), 201',
|
||||||
|
'Sipprelle, Dwight (The Aerie), 202',
|
||||||
|
'Sipprelle -lot, 203',
|
||||||
|
'Rusten, Brendt (Take Five), 204A',
|
||||||
|
'Useppa One LLC (Rusten), 204B',
|
||||||
|
'Useppa One LLC (Rusten)- gazebo, 205',
|
||||||
|
'Mezyninski, Steve (Hill Tide), 206',
|
||||||
|
'Sullivan, 207',
|
||||||
|
'Sullivan, 208',
|
||||||
|
'Nutting (A Little Piece of Heaven), 209',
|
||||||
|
'UIDC - Donald (Live Oak Cottage), 210',
|
||||||
|
'Newbold, Carol (Palm Cottage), 211',
|
||||||
|
'Dimmitt, Peter (Lookout Cottage), 212',
|
||||||
|
'Dimmitt, Peter (Lookout Cottage), 213',
|
||||||
|
'Greenwell (Hide-A-Way Cottage), 214',
|
||||||
|
'Hughes, Dave (Cottage 15), 215',
|
||||||
|
'Carmichael, Michael (8 Palms), 216',
|
||||||
|
'Anderson, Donna (Island Time), 217',
|
||||||
|
'Hitchcox, Doug, 218',
|
||||||
|
'Lockhart, Kim (Coquina Cottage), 301A',
|
||||||
|
'Ryan, Doug & Cindy, 301B',
|
||||||
|
'Duke - lot, 302',
|
||||||
|
'Stuart, Deb, 303A',
|
||||||
|
'Judge, James (Osprey Point), 303B',
|
||||||
|
'Fassett, Ladd (Cottage 3AN), 303C',
|
||||||
|
'Ink (Cottage 3AS), 303D',
|
||||||
|
'McGinn (Gaspar Island), 304',
|
||||||
|
'McGinn (Gaspar Island), 305',
|
||||||
|
'Wendorf, Bruce & Hillary (It\'s a Wendorful Life Cottage), 306',
|
||||||
|
'Bennett, 307',
|
||||||
|
'Hanford, Ken & Kim, 308',
|
||||||
|
'Conner, Drew & Paige (Campfire Lighter), 309',
|
||||||
|
'Bentley, Joel & Sue, 310',
|
||||||
|
'Covington/Bernard, 311A',
|
||||||
|
'O\'Connell (Often Inn), 311B',
|
||||||
|
'McColgan, Brian (La Costa Sunset), 312',
|
||||||
|
'Preckwinkle, George, 313',
|
||||||
|
'Perrone (Lagoona Vista), 314',
|
||||||
|
'Thompson, Jim, 315',
|
||||||
|
'Nutting - lot, 316',
|
||||||
|
'Nutting - lot, 317',
|
||||||
|
'Kent / Williamson (Kaos Kattage), 318',
|
||||||
|
'Kahane -lot, 319',
|
||||||
|
'Kahane (Belvedere Cottage?), 320',
|
||||||
|
'Nutting - lot, 321',
|
||||||
|
'Molosky, Andrew, 322A',
|
||||||
|
'Folkerth, Betty (Living Water Cottage), 322B',
|
||||||
|
'Rowars, Chuck (Mangoes), 323',
|
||||||
|
'Eldemir, Alex, 324',
|
||||||
|
'Paradise Partnership, 325',
|
||||||
|
'Ricciardelli - lot, 326',
|
||||||
|
'Cardwell - lot, 327',
|
||||||
|
'Spencer, Doug & Holly, 328',
|
||||||
|
'Walker, Jeff & Alicia (HighSeas), 329',
|
||||||
|
'Loeks - lot, 401',
|
||||||
|
'Loeks - lot, 402',
|
||||||
|
'Loeks - lot, 403',
|
||||||
|
'Wilson, George - lot, 404',
|
||||||
|
'Nutting - lot, 405',
|
||||||
|
'Nutting - lot, 406',
|
||||||
|
'Soriero - lot, 407',
|
||||||
|
'Alderman, Gary, 501',
|
||||||
|
'Dockery, Bob & Susan, 502',
|
||||||
|
'Hansen (Starfish Cottage), 503',
|
||||||
|
'Berger / Bevis (Castaway), 504',
|
||||||
|
'Tinney, John, 505',
|
||||||
|
'Matter, John, 506',
|
||||||
|
'Stuart (Fancy Free), 507',
|
||||||
|
'Fernandez, 508',
|
||||||
|
'Wiesen, Matt & Katy, 509',
|
||||||
|
'Romine, Dave, 510',
|
||||||
|
'Handin (Grandview Cottage), 511',
|
||||||
|
'Colgan, Tony (Cottage 12), 512',
|
||||||
|
'Cupello (White Rose Cottage), 513',
|
||||||
|
'Meyer (Burgee Cottage), 514',
|
||||||
|
'Jarvis, Jim & Patty (Laguna Cottage), 515',
|
||||||
|
'Wilson, George (Cottage 16), 516',
|
||||||
|
'Shimp, Kevin, 517',
|
||||||
|
'Dreher, Brucke (Island Escape), 518',
|
||||||
|
'Beckman (Mangrove Cottage), 519',
|
||||||
|
'Gatewood (Water Wings), 520',
|
||||||
|
'Duke, Steve & Janie (Dutchess Cottage), 521',
|
||||||
|
'Useppa Island LLC, 522',
|
||||||
|
'Mercurio, Michael & Beth (Osprey Nest), 523',
|
||||||
|
'Compton, Janie (Dvorak), 524',
|
||||||
];
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function authenticate(req, res, next) {
|
|||||||
* Admin-only middleware — must be called after authenticate
|
* Admin-only middleware — must be called after authenticate
|
||||||
*/
|
*/
|
||||||
function requireAdmin(req, res, next) {
|
function requireAdmin(req, res, next) {
|
||||||
if (!req.user || (req.user.role !== 'admin' && req.user.role !== 'super_admin')) {
|
if (!req.user || !['office', 'admin', 'super_admin'].includes(req.user.role)) {
|
||||||
return res.status(403).json({ error: 'Admin access required' });
|
return res.status(403).json({ error: 'Admin access required' });
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const registerSchema = z.object({
|
|||||||
.min(8, 'Password must be at least 8 characters')
|
.min(8, 'Password must be at least 8 characters')
|
||||||
.max(128),
|
.max(128),
|
||||||
name: z.string().min(1, 'Name is required').max(100).trim(),
|
name: z.string().min(1, 'Name is required').max(100).trim(),
|
||||||
role: z.enum(['employee', 'admin', 'super_admin']).default('employee'),
|
role: z.enum(['employee', 'office', 'admin', 'super_admin']).default('employee'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshSchema = z.object({
|
const refreshSchema = z.object({
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,6 @@
|
|||||||
DB_PASSWORD=coastal_secret
|
DB_PASSWORD=coastal_secret
|
||||||
# Set to "true" to enable daily pg_dump backups
|
JWT_SECRET=Ts8pLm3QvK5nRw9sYzB4jFc7hE0aGd2U
|
||||||
|
JWT_REFRESH_SECRET=Rf6kMn2QpT8wLs4vYzA7jFb9hD0eGc5U
|
||||||
|
NODE_ENV=production
|
||||||
|
CORS_ORIGINS=https://ts.bizzle.cloud
|
||||||
ENABLE_BACKUPS=false
|
ENABLE_BACKUPS=false
|
||||||
BACKUP_DIR=/backups
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ COPY --from=builder /app/prisma ./prisma
|
|||||||
COPY --from=builder /app/src ./src
|
COPY --from=builder /app/src ./src
|
||||||
COPY --from=builder /app/package.json ./
|
COPY --from=builder /app/package.json ./
|
||||||
|
|
||||||
|
# Fix ownership so non-root user can regenerate Prisma client
|
||||||
|
RUN chown -R coastal:coastal /app
|
||||||
|
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER coastal
|
USER coastal
|
||||||
|
|
||||||
|
|||||||
+29
-19
@@ -1,19 +1,16 @@
|
|||||||
version: '3.9'
|
name: timesheet
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# ─── PostgreSQL ─────────────────────────────────────────
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: coastal-db
|
container_name: timesheet-db
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: coastal
|
POSTGRES_USER: coastal
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-coastal_secret}
|
POSTGRES_PASSWORD: ${DB_PASSWORD:-coastal_secret}
|
||||||
POSTGRES_DB: coastal_timesheet
|
POSTGRES_DB: coastal_timesheet
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
ports:
|
|
||||||
- '127.0.0.1:5432:5432'
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD-SHELL', 'pg_isready -U coastal -d coastal_timesheet']
|
test: ['CMD-SHELL', 'pg_isready -U coastal -d coastal_timesheet']
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -22,14 +19,17 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
networks:
|
networks:
|
||||||
- coastal
|
- coastal
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
# ─── Backend API ────────────────────────────────────────
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: docker/backend/Dockerfile
|
dockerfile: docker/backend/Dockerfile
|
||||||
container_name: coastal-backend
|
container_name: timesheet-backend
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -37,9 +37,9 @@ services:
|
|||||||
DATABASE_URL: postgresql://coastal:${DB_PASSWORD:-coastal_secret}@db:5432/coastal_timesheet
|
DATABASE_URL: postgresql://coastal:${DB_PASSWORD:-coastal_secret}@db:5432/coastal_timesheet
|
||||||
JWT_SECRET: ${JWT_SECRET:-change-me-in-production-jwt-secret-2026}
|
JWT_SECRET: ${JWT_SECRET:-change-me-in-production-jwt-secret-2026}
|
||||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-change-me-in-production-refresh-secret-2026}
|
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-change-me-in-production-refresh-secret-2026}
|
||||||
PORT: '3001'
|
PORT: '3004'
|
||||||
NODE_ENV: ${NODE_ENV:-production}
|
NODE_ENV: production
|
||||||
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost,http://localhost:3000,http://100.94.106.120:8080}
|
CORS_ORIGINS: https://ts.bizzle.cloud,http://localhost
|
||||||
SMTP_HOST: ${SMTP_HOST:-}
|
SMTP_HOST: ${SMTP_HOST:-}
|
||||||
SMTP_PORT: ${SMTP_PORT:-587}
|
SMTP_PORT: ${SMTP_PORT:-587}
|
||||||
SMTP_USER: ${SMTP_USER:-}
|
SMTP_USER: ${SMTP_USER:-}
|
||||||
@@ -47,28 +47,33 @@ services:
|
|||||||
SMTP_FROM: ${SMTP_FROM:-}
|
SMTP_FROM: ${SMTP_FROM:-}
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-bizzle@coastalcontracting.com}
|
ADMIN_EMAIL: ${ADMIN_EMAIL:-bizzle@coastalcontracting.com}
|
||||||
ports:
|
ports:
|
||||||
- '127.0.0.1:3001:3001'
|
- '127.0.0.1:3004:3004'
|
||||||
|
- '172.18.0.1:3004:3004'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'node', '-e', "fetch('http://localhost:3001/api/health').then(r=>{if(!r.ok)throw 1}).catch(()=>process.exit(1))"]
|
test: ['CMD', 'node', '-e', "fetch('http://localhost:3004/api/health').then(r=>{if(!r.ok)throw 1}).catch(()=>process.exit(1))"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
networks:
|
networks:
|
||||||
- coastal
|
- coastal
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
# ─── Frontend (Nginx) ──────────────────────────────────
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: docker/frontend/Dockerfile
|
dockerfile: docker/frontend/Dockerfile
|
||||||
container_name: coastal-frontend
|
container_name: timesheet-frontend
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
backend:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- '100.94.106.120:8080:80'
|
- '127.0.0.1:8083:80'
|
||||||
|
- '172.18.0.1:8083:80'
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -79,10 +84,15 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
networks:
|
networks:
|
||||||
- coastal
|
- coastal
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
driver: local
|
external: true
|
||||||
|
name: coastal_pgdata
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
coastal:
|
coastal:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# /* → frontend static files
|
# /* → frontend static files
|
||||||
|
|
||||||
upstream backend_api {
|
upstream backend_api {
|
||||||
server backend:3001;
|
server backend:3004;
|
||||||
keepalive 16;
|
keepalive 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,35 @@ function getRefreshToken() {
|
|||||||
return localStorage.getItem('refreshToken');
|
return localStorage.getItem('refreshToken');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJwt(token) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(atob(token.split('.')[1]));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setTokens(accessToken, refreshToken) {
|
function setTokens(accessToken, refreshToken) {
|
||||||
localStorage.setItem('accessToken', accessToken);
|
localStorage.setItem('accessToken', accessToken);
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
localStorage.setItem('refreshToken', refreshToken);
|
localStorage.setItem('refreshToken', refreshToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync the 'user' object in localStorage with the data from the new access token
|
||||||
|
const payload = parseJwt(accessToken);
|
||||||
|
if (payload) {
|
||||||
|
const existingUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
|
// Ensure we don't overwrite user-specific static data if missing from JWT
|
||||||
|
const updatedUser = { ...existingUser, ...payload };
|
||||||
|
// The user ID usually comes as 'sub' in JWT, but frontend expects 'id'
|
||||||
|
if (payload.sub && !updatedUser.id) updatedUser.id = payload.sub;
|
||||||
|
|
||||||
|
// Explicitly update role and email which are crucial for the UI
|
||||||
|
if (payload.role) updatedUser.role = payload.role;
|
||||||
|
if (payload.email) updatedUser.email = payload.email;
|
||||||
|
|
||||||
|
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTokens() {
|
function clearTokens() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import api, { setTokens, clearTokens, getAccessToken } from '../api/client';
|
import api, { setTokens, clearTokens, getAccessToken } from '../api/client';
|
||||||
|
|
||||||
const AuthContext = createContext(null);
|
const AuthContext = createContext(null);
|
||||||
@@ -13,6 +14,17 @@ export function AuthProvider({ children }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const refreshUser = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/auth/me');
|
||||||
|
setUser(data.user);
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
} catch {
|
||||||
|
/* If /me fails, we might be logged out or server is down */
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
/* On mount, verify the stored token is still valid */
|
/* On mount, verify the stored token is still valid */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -36,6 +48,13 @@ export function AuthProvider({ children }) {
|
|||||||
verify();
|
verify();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/* Refresh user profile on navigation to ensure role is up to date */
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
refreshUser();
|
||||||
|
}
|
||||||
|
}, [location.pathname, user?.id]);
|
||||||
|
|
||||||
const login = useCallback(async (email, password) => {
|
const login = useCallback(async (email, password) => {
|
||||||
const { data } = await api.post('/auth/login', { email, password });
|
const { data } = await api.post('/auth/login', { email, password });
|
||||||
setTokens(data.accessToken, data.refreshToken);
|
setTokens(data.accessToken, data.refreshToken);
|
||||||
@@ -54,7 +73,7 @@ export function AuthProvider({ children }) {
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isAdmin = user?.role === 'admin' || user?.role === 'super_admin';
|
const isAdmin = ['office', 'admin', 'super_admin'].includes(user?.role);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import api from '../api/client';
|
import api from '../api/client';
|
||||||
import StatusBadge from '../components/StatusBadge';
|
import StatusBadge from '../components/StatusBadge';
|
||||||
import { Check, X, Users, Home, Clock, Loader2, Plus, UserPlus, Download, BarChart3, TrendingUp, AlertTriangle } from 'lucide-react';
|
import { Check, X, Users, Home, Clock, Loader2, Plus, UserPlus, Download, BarChart3, TrendingUp, AlertTriangle } from 'lucide-react';
|
||||||
@@ -192,6 +193,7 @@ function PendingReviews() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ManageUsers() {
|
function ManageUsers() {
|
||||||
|
const { user } = useAuth();
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
@@ -244,7 +246,8 @@ function ManageUsers() {
|
|||||||
<input value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required type="password" placeholder="Password (min 8 chars)" minLength={8} className="px-3 py-2.5 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm text-gray-900 dark:text-white" />
|
<input value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required type="password" placeholder="Password (min 8 chars)" minLength={8} className="px-3 py-2.5 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm text-gray-900 dark:text-white" />
|
||||||
<select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })} className="px-3 py-2.5 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm text-gray-900 dark:text-white">
|
<select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })} className="px-3 py-2.5 rounded-xl bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-sm text-gray-900 dark:text-white">
|
||||||
<option value="employee">Employee</option>
|
<option value="employee">Employee</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="office">Office Staff</option>
|
||||||
|
{user?.role === 'super_admin' && <option value="super_admin">Admin</option>}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
@@ -275,7 +278,8 @@ function ManageUsers() {
|
|||||||
className="text-xs px-2 py-1 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
|
className="text-xs px-2 py-1 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<option value="employee">Employee</option>
|
<option value="employee">Employee</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="office">Office Staff</option>
|
||||||
|
{user?.role === 'super_admin' && <option value="super_admin">Admin</option>}
|
||||||
</select>
|
</select>
|
||||||
{u.isActive === false ? (
|
{u.isActive === false ? (
|
||||||
<button
|
<button
|
||||||
@@ -726,6 +730,7 @@ function OvertimeReport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Admin() {
|
export default function Admin() {
|
||||||
|
const { user } = useAuth();
|
||||||
const [tab, setTab] = useState('reviews');
|
const [tab, setTab] = useState('reviews');
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user