diff --git a/map/TEMP_USER_IMPLEMENTATION.md b/map/TEMP_USER_IMPLEMENTATION.md new file mode 100644 index 0000000..8a34677 --- /dev/null +++ b/map/TEMP_USER_IMPLEMENTATION.md @@ -0,0 +1,115 @@ +# Temp User Implementation Guide + +## Database Schema Changes Required + +To implement the temp user type functionality, you need to add the following columns to your NocoDB Login table: + +### Required Columns: + +1. **UserType** (Single Select) + - Options: "admin", "user", "temp" + - Default: "user" + - Description: Defines the user's permission level + +### Optional Columns for Time-Based Expiration: + +2. **ExpiresAt** (DateTime, nullable) + - When the account expires (for temp users) + +3. **CreatedAt** (DateTime, default: now()) + - When the account was created + +4. **ExpireDays** (Integer, nullable) + - Number of days until expiration (set by admin) + +## Temp User Permissions + +### ✅ Allowed Actions: +- Login and view map (if not expired) +- Add new locations +- Edit existing locations + +### ❌ Restricted Actions: +- Delete locations +- Access shifts page (/shifts.html) +- Access user profile page (/user.html) +- Access admin panel (/admin.html) +- Search database (only documentation search available) +- Move location markers +- **Login after expiration date** (expired temp users are blocked) + +## Expiration Validation + +The system now includes comprehensive expiration validation for temp users: + +1. **Login Validation**: Expired temp users cannot login +2. **Session Validation**: Expired temp users are automatically logged out +3. **Middleware Checks**: All authenticated routes verify temp user expiration +4. **Frontend Handling**: Expired users receive clear error messages + +### Expiration Flow: +1. User attempts login → System checks if temp user is expired → Blocks login if expired +2. Authenticated user makes request → Middleware checks expiration → Logs out if expired +3. Frontend auth check → Detects expiration → Shows message and redirects to login + +## Implementation Summary + +The implementation adds: + +1. **Backend Changes:** + - New middleware functions: `requireNonTemp`, `requireDeletePermission` + - Updated auth controller to handle `userType` in sessions + - **Expiration validation during login** (prevents expired temp users from logging in) + - **Session expiration checks** in all auth middleware + - Protected routes for shifts and user pages + - Updated users controller to support user type and expiration + - Optional account expiration service + +2. **Frontend Changes:** + - User type checking in authentication + - **Expiration handling** in auth check with user feedback + - Conditional UI element hiding for temp users + - Restricted search functionality + - Visual indicators (temp badge) + - Updated admin panel for creating temp users + - **Login page expiration message** display + +3. **Admin Panel Enhancements:** + - User type selection dropdown (admin/user/temp) + - Expiration days field for temp users + - Enhanced user table with type and expiration display + - Visual indicators for expiring accounts + +4. **Database Integration:** + - Session storage of user type + - User type validation during login + - Optional expiration date handling + +## Testing Checklist + +1. Create test users in NocoDB with different UserType values +2. Test login with each user type +3. **Test that expired temp users cannot login** +4. **Test that expired temp users are logged out during session** +5. Verify temp users cannot access restricted features +6. Test that temp users can add and edit but not delete locations +7. Confirm UI elements are properly hidden for temp users +8. **Verify expiration messages are displayed correctly** +9. **Test admin panel temp user creation with expiration dates** + +## Security Notes + +- Temp users have limited permissions enforced at both frontend and backend levels +- All restricted routes return 403 errors for temp users +- **Expired temp users are blocked from login and automatically logged out** +- **Expiration validation occurs at multiple checkpoints** (login, middleware, auth check) +- Session includes userType for authorization checks +- Frontend restrictions are backed by server-side validation +- **Clear user feedback for expired accounts** prevents confusion + +## Future Enhancements + +- Email notifications before account expiration +- Bulk management of temp accounts +- Admin dashboard widgets for temp account monitoring +- Configurable default expiration periods diff --git a/map/TEMP_USER_TEST.md b/map/TEMP_USER_TEST.md new file mode 100644 index 0000000..19f2947 --- /dev/null +++ b/map/TEMP_USER_TEST.md @@ -0,0 +1,146 @@ +# Temp User Implementation Test Guide + +## Testing the Implementation + +### 1. Database Setup +Before testing, ensure your NocoDB Login table has these columns: +- `UserType` (Single Select: admin, user, temp) +- `ExpiresAt` (DateTime, nullable) +- `CreatedAt` (DateTime) +- `ExpireDays` (Integer, nullable) + +### 2. Test User Creation via Admin Panel + +1. **Access Admin Panel** + - Login as an admin user + - Navigate to `/admin.html` + - Go to the "Users" section + +2. **Create Regular User** + - Email: `testuser@example.com` + - Name: `Test User` + - Password: `password123` + - User Type: `Regular User` + - Click "Create User" + +3. **Create Temp User** + - Email: `tempuser@example.com` + - Name: `Temp User` + - Password: `password123` + - User Type: `Temporary User` + - Expires After: `30` days + - Click "Create User" + +4. **Create Admin User** + - Email: `adminuser@example.com` + - Name: `Admin User` + - Password: `password123` + - User Type: `Admin` + - Click "Create User" + +### 3. Test User Permissions + +#### Test Temp User Restrictions: + +1. **Login as temp user** (`tempuser@example.com`) + +2. **Verify UI Elements Hidden:** + - No "Shifts" link in navigation + - No "Profile" link in navigation + - User email shows "Temp" badge + - Map search only shows "docs" mode (no database search) + +3. **Test Location Operations:** + - ✅ **Add Location**: Should work + - ✅ **Edit Location**: Should work + - ❌ **Delete Location**: Delete button should be hidden in edit form + - ❌ **Move Location**: Move button should be hidden in popup + +4. **Test Restricted Access:** + - Navigate to `/shifts.html` → Should redirect or show 403 + - Navigate to `/user.html` → Should redirect or show 403 + - Navigate to `/admin.html` → Should redirect or show 403 + +#### Test Regular User: + +1. **Login as regular user** (`testuser@example.com`) + +2. **Verify Full Access:** + - ✅ Can access shifts page + - ✅ Can access user profile + - ✅ Can add, edit, and delete locations + - ✅ Can use database search + - ❌ Cannot access admin panel + +#### Test Admin User: + +1. **Login as admin user** (`adminuser@example.com`) + +2. **Verify Admin Access:** + - ✅ Full access to all features + - ✅ Can access admin panel + - ✅ Can create/manage users + +### 4. Test Backend API Endpoints + +Use browser console or testing tool: + +```javascript +// Test temp user cannot delete location +fetch('/api/locations/1', { method: 'DELETE' }) +.then(r => r.json()) +.then(console.log); // Should return 403 error for temp users + +// Test temp user cannot access shifts +fetch('/api/shifts') +.then(r => r.json()) +.then(console.log); // Should return 403 error for temp users +``` + +### 5. Expected Results + +#### User Table Display: +- Regular User: Blue "User" badge +- Temp User: Orange "Temp" badge + expiration date +- Admin User: Green "Admin" badge + +#### Authentication Response: +```json +{ + "authenticated": true, + "user": { + "email": "tempuser@example.com", + "name": "Temp User", + "isAdmin": false, + "userType": "temp" + } +} +``` + +### 6. Troubleshooting + +**If temp user can access restricted features:** +- Check middleware is properly imported in routes +- Verify session includes `userType` +- Check browser console for JavaScript errors + +**If user creation fails:** +- Verify NocoDB table has required columns +- Check server logs for database errors +- Ensure column names match exactly + +**If UI elements not hiding:** +- Check browser console for auth errors +- Verify `currentUser.userType` is set +- Check CSS classes are applied correctly + +### 7. Security Verification + +Temp users should receive **403 Forbidden** responses for: +- `DELETE /api/locations/:id` +- `GET /shifts.html` +- `GET /user.html` +- `GET /admin.html` +- `GET /api/shifts` + +All restrictions should be enforced server-side, not just hidden in UI. diff --git a/map/app/controllers/authController.js b/map/app/controllers/authController.js index b2cfb17..1e34d15 100644 --- a/map/app/controllers/authController.js +++ b/map/app/controllers/authController.js @@ -50,7 +50,25 @@ class AuthController { error: 'Invalid email or password' }); } - + + // Check if temp user has expired + const userType = user.UserType || user.userType || 'user'; + if (userType === 'temp') { + const expiration = user.ExpiresAt || user.expiresAt || user.Expiration || user.expiration; + if (expiration) { + const expirationDate = new Date(expiration); + const now = new Date(); + + if (now > expirationDate) { + logger.warn(`Expired temp user attempted login: ${email}, expired: ${expiration}`); + return res.status(401).json({ + success: false, + error: 'Account has expired. Please contact an administrator.' + }); + } + } + } + // Update last login time try { const userId = extractId(user); @@ -73,6 +91,7 @@ class AuthController { req.session.userEmail = user.email || user.Email; // Make sure this is set req.session.userName = user.name || user.Name; req.session.isAdmin = user.admin || user.Admin || false; + req.session.userType = user.UserType || user.userType || (req.session.isAdmin ? 'admin' : 'user'); logger.info('User logged in:', { email: req.session.userEmail, @@ -97,7 +116,8 @@ class AuthController { user: { email: email, name: req.session.userName, - isAdmin: req.session.isAdmin + isAdmin: req.session.isAdmin, + userType: req.session.userType } }); }); @@ -128,12 +148,48 @@ class AuthController { } async check(req, res) { + // If user is authenticated, check for temp user expiration + if (req.session?.authenticated && req.session?.userType === 'temp' && req.session?.userEmail) { + try { + const user = await nocodbService.getUserByEmail(req.session.userEmail); + if (user) { + const expiration = user.ExpiresAt || user.ExpiresAt || user.Expiration || user.expiration; + if (expiration) { + const expirationDate = new Date(expiration); + const now = new Date(); + + if (now > expirationDate) { + logger.warn(`Expired temp user session detected in check: ${req.session.userEmail}, expired: ${expiration}`); + + // Destroy the session + req.session.destroy((err) => { + if (err) { + logger.error('Session destroy error:', err); + } + }); + + return res.json({ + authenticated: false, + user: null, + expired: true, + message: 'Account has expired. Please contact an administrator.' + }); + } + } + } + } catch (error) { + logger.error('Error checking temp user expiration in check:', error.message); + // Don't fail the check on database errors, just log it + } + } + res.json({ authenticated: req.session?.authenticated || false, user: req.session?.authenticated ? { email: req.session.userEmail, name: req.session.userName, - isAdmin: req.session.isAdmin || false + isAdmin: req.session.isAdmin || false, + userType: req.session.userType || 'user' } : null }); } diff --git a/map/app/controllers/usersController.js b/map/app/controllers/usersController.js index 7da2f64..b420ddf 100644 --- a/map/app/controllers/usersController.js +++ b/map/app/controllers/usersController.js @@ -55,7 +55,7 @@ class UsersController { async create(req, res) { try { - const { email, password, name, admin } = req.body; + const { email, password, name, isAdmin, userType, expireDays } = req.body; if (!email || !password) { return res.status(400).json({ @@ -81,6 +81,14 @@ class UsersController { }); } + // Calculate expiration date for temp users + let expiresAt = null; + if (userType === 'temp' && expireDays) { + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + expireDays); + expiresAt = expirationDate.toISOString(); + } + // Create new user - use the actual column names from your table const userData = { Email: email, @@ -89,9 +97,13 @@ class UsersController { password: password, Name: name || '', name: name || '', - Admin: admin === true, - admin: admin === true - // Removed created_at fields as they might not exist + Admin: isAdmin === true, + admin: isAdmin === true, + UserType: userType || 'user', + userType: userType || 'user', + CreatedAt: new Date().toISOString(), + ExpiresAt: expiresAt, + ExpireDays: userType === 'temp' ? expireDays : null }; const response = await nocodbService.create( @@ -106,7 +118,9 @@ class UsersController { id: extractId(response), email: email, name: name, - admin: admin + admin: isAdmin, + userType: userType, + expiresAt: expiresAt } }); diff --git a/map/app/middleware/auth.js b/map/app/middleware/auth.js index 2741411..f22c1ff 100644 --- a/map/app/middleware/auth.js +++ b/map/app/middleware/auth.js @@ -1,11 +1,59 @@ const logger = require('../utils/logger'); +const nocodbService = require('../services/nocodb'); -const requireAuth = (req, res, next) => { +// Helper function to check if a temp user has expired +const checkTempUserExpiration = async (req, res) => { + if (req.session?.userType === 'temp' && req.session?.userEmail) { + try { + const user = await nocodbService.getUserByEmail(req.session.userEmail); + if (user) { + const expiration = user.ExpiresAt || user.expiresAt || user.Expiration || user.expiration; + if (expiration) { + const expirationDate = new Date(expiration); + const now = new Date(); + + if (now > expirationDate) { + logger.warn(`Expired temp user session detected: ${req.session.userEmail}, expired: ${expiration}`); + + // Destroy the session + req.session.destroy((err) => { + if (err) { + logger.error('Session destroy error:', err); + } + }); + + if (req.xhr || req.headers.accept?.indexOf('json') > -1) { + return res.status(401).json({ + success: false, + error: 'Account has expired. Please contact an administrator.', + expired: true + }); + } else { + return res.redirect('/login.html?expired=true'); + } + } + } + } + } catch (error) { + logger.error('Error checking temp user expiration:', error.message); + // Don't fail the request on database errors, just log it + } + } + return null; // No expiration issue +}; + +const requireAuth = async (req, res, next) => { // Check for both authentication patterns used in your app const isAuthenticated = (req.session && req.session.authenticated) || (req.session && req.session.userId && req.session.userEmail); if (isAuthenticated) { + // Check if temp user has expired + const expirationResponse = await checkTempUserExpiration(req, res); + if (expirationResponse) { + return; // Response already sent by checkTempUserExpiration + } + next(); } else { logger.warn('Unauthorized access attempt', { @@ -25,12 +73,18 @@ const requireAuth = (req, res, next) => { } }; -const requireAdmin = (req, res, next) => { +const requireAdmin = async (req, res, next) => { // Check for both authentication patterns used in your app const isAuthenticated = (req.session && req.session.authenticated) || (req.session && req.session.userId && req.session.userEmail); if (isAuthenticated && req.session.isAdmin) { + // Check if temp user has expired + const expirationResponse = await checkTempUserExpiration(req, res); + if (expirationResponse) { + return; // Response already sent by checkTempUserExpiration + } + next(); } else { logger.warn('Unauthorized admin access attempt', { @@ -51,7 +105,68 @@ const requireAdmin = (req, res, next) => { } }; +const requireNonTemp = async (req, res, next) => { + const isAuthenticated = (req.session && req.session.authenticated) || + (req.session && req.session.userId && req.session.userEmail); + + if (isAuthenticated && req.session.userType !== 'temp') { + // Check if temp user has expired (shouldn't happen here, but for safety) + const expirationResponse = await checkTempUserExpiration(req, res); + if (expirationResponse) { + return; // Response already sent by checkTempUserExpiration + } + + next(); + } else { + logger.warn('Temp user access denied', { + ip: req.ip, + path: req.path, + user: req.session?.userEmail || 'anonymous', + userType: req.session?.userType || 'unknown' + }); + + if (req.xhr || req.headers.accept?.indexOf('json') > -1) { + res.status(403).json({ + success: false, + error: 'Access denied for temporary users' + }); + } else { + res.redirect('/'); + } + } +}; + +const requireDeletePermission = async (req, res, next) => { + const isAuthenticated = (req.session && req.session.authenticated) || + (req.session && req.session.userId && req.session.userEmail); + + // Only admins and regular users can delete, not temps + if (isAuthenticated && req.session.userType !== 'temp') { + // Check if temp user has expired (shouldn't happen here, but for safety) + const expirationResponse = await checkTempUserExpiration(req, res); + if (expirationResponse) { + return; // Response already sent by checkTempUserExpiration + } + + next(); + } else { + logger.warn('Delete permission denied', { + ip: req.ip, + path: req.path, + user: req.session?.userEmail || 'anonymous', + userType: req.session?.userType || 'unknown' + }); + + res.status(403).json({ + success: false, + error: 'Delete permission denied' + }); + } +}; + module.exports = { requireAuth, - requireAdmin + requireAdmin, + requireNonTemp, + requireDeletePermission }; \ No newline at end of file diff --git a/map/app/public/admin.html b/map/app/public/admin.html index 032752b..dd4c5ca 100644 --- a/map/app/public/admin.html +++ b/map/app/public/admin.html @@ -422,10 +422,23 @@ +
+ + +
+
diff --git a/map/app/public/css/admin.css b/map/app/public/css/admin.css index 7aff681..aa244d7 100644 --- a/map/app/public/css/admin.css +++ b/map/app/public/css/admin.css @@ -815,6 +815,44 @@ color: white; } +.user-role.temp { + background-color: #ff9800; + color: white; +} + +.expiration-info { + display: inline-block; + margin-left: 8px; + font-size: 0.8em; + color: #666; + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; +} + +.expiration-warning { + color: #ff5722; + font-weight: bold; +} + +.expires-soon { + background-color: #fff3cd; + border-left: 4px solid #ffc107; +} + +.expired { + background-color: #f8d7da; + border-left: 4px solid #dc3545; + opacity: 0.7; +} + +.help-text { + font-size: 0.85em; + color: #666; + margin-top: 4px; + display: block; +} + .user-actions { display: flex; gap: 8px; diff --git a/map/app/public/css/modules/base.css b/map/app/public/css/modules/base.css index 5fbe02f..aeb1ba8 100644 --- a/map/app/public/css/modules/base.css +++ b/map/app/public/css/modules/base.css @@ -54,3 +54,22 @@ body { width: 100%; overflow-x: hidden; /* Prevent horizontal scrolling */ } + +/* Badge styles */ +.badge { + display: inline-block; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + font-weight: 500; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.375rem; + margin-left: 0.5rem; +} + +.temp-badge { + background-color: var(--warning-color); + color: white; +} diff --git a/map/app/public/css/modules/temp-user.css b/map/app/public/css/modules/temp-user.css new file mode 100644 index 0000000..7390ffc --- /dev/null +++ b/map/app/public/css/modules/temp-user.css @@ -0,0 +1,31 @@ +/* Temp User Restrictions */ +.temp-badge { + display: inline-block; + padding: 2px 8px; + background-color: #ff9800; + color: white; + font-size: 11px; + font-weight: bold; + border-radius: 3px; + margin-left: 5px; + text-transform: uppercase; +} + +/* Hide restricted elements for temp users */ +.temp-restricted { + display: none !important; +} + +/* More specific selectors to ensure hiding */ +a.temp-restricted, +.btn.temp-restricted, +.header-actions .temp-restricted, +.mobile-dropdown-item.temp-restricted { + display: none !important; +} + +/* Hide Shift links/buttons whenever body has .temp-user */ +body.temp-user a[href="/shifts.html"], +body.temp-user .mobile-dropdown-item a[href="/shifts.html"] { + display: none !important; +} \ No newline at end of file diff --git a/map/app/public/css/style.css b/map/app/public/css/style.css index 31cc7f9..cdf5b13 100644 --- a/map/app/public/css/style.css +++ b/map/app/public/css/style.css @@ -16,3 +16,4 @@ @import url("modules/cache-busting.css"); @import url("modules/apartment-popup.css"); @import url("modules/apartment-marker.css"); +@import url("modules/temp-user.css") diff --git a/map/app/public/index.html b/map/app/public/index.html index 716f45c..bff6e62 100644 --- a/map/app/public/index.html +++ b/map/app/public/index.html @@ -54,7 +54,7 @@ Homepage - + 📅 View Shifts diff --git a/map/app/public/js/admin.js b/map/app/public/js/admin.js index b059351..312e3c2 100644 --- a/map/app/public/js/admin.js +++ b/map/app/public/js/admin.js @@ -358,6 +358,30 @@ function setupEventListeners() { if (clearUserBtn) { clearUserBtn.addEventListener('click', clearUserForm); } + + // User type change listener + const userTypeSelect = document.getElementById('user-type'); + if (userTypeSelect) { + userTypeSelect.addEventListener('change', (e) => { + const expirationGroup = document.getElementById('expiration-group'); + const isAdminCheckbox = document.getElementById('user-is-admin'); + + if (e.target.value === 'temp') { + expirationGroup.style.display = 'block'; + isAdminCheckbox.checked = false; + isAdminCheckbox.disabled = true; + } else { + expirationGroup.style.display = 'none'; + isAdminCheckbox.disabled = false; + + if (e.target.value === 'admin') { + isAdminCheckbox.checked = true; + } else { + isAdminCheckbox.checked = false; + } + } + }); + } } // Setup navigation between admin sections @@ -1282,16 +1306,34 @@ function displayUsers(users) { const createdDate = user.created_at || user['Created At'] || user.createdAt; const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A'; const isAdmin = user.admin || user.Admin || false; + const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user'); const userId = user.Id || user.id || user.ID; + + // Handle expiration info + let expirationInfo = ''; + if (user.ExpiresAt) { + const expirationDate = new Date(user.ExpiresAt); + const now = new Date(); + const daysUntilExpiration = Math.floor((expirationDate - now) / (1000 * 60 * 60 * 24)); + + if (daysUntilExpiration < 0) { + expirationInfo = `Expired ${Math.abs(daysUntilExpiration)} days ago`; + } else if (daysUntilExpiration <= 3) { + expirationInfo = `Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}`; + } else { + expirationInfo = `Expires: ${expirationDate.toLocaleDateString()}`; + } + } return ` - + ${escapeHtml(user.email || user.Email || 'N/A')} ${escapeHtml(user.name || user.Name || 'N/A')} - - ${isAdmin ? 'Admin' : 'User'} + + ${userType.charAt(0).toUpperCase() + userType.slice(1)} + ${expirationInfo} ${formattedDate} @@ -1401,6 +1443,9 @@ async function createUser(e) { const email = document.getElementById('user-email').value.trim(); const password = document.getElementById('user-password').value; const name = document.getElementById('user-name').value.trim(); + const userType = document.getElementById('user-type').value; + const expireDays = userType === 'temp' ? + parseInt(document.getElementById('user-expire-days').value) : null; const admin = document.getElementById('user-is-admin').checked; if (!email || !password) { @@ -1413,18 +1458,27 @@ async function createUser(e) { return; } + if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) { + showStatus('Expiration days must be between 1 and 365 for temporary users', 'error'); + return; + } + try { + const userData = { + email, + password, + name: name || '', + isAdmin: userType === 'admin' || admin, + userType, + expireDays + }; + const response = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email, - password, - name: name || '', - admin - }) + body: JSON.stringify(userData) }); const data = await response.json(); @@ -1447,6 +1501,25 @@ function clearUserForm() { const form = document.getElementById('create-user-form'); if (form) { form.reset(); + + // Reset user type to default + const userTypeSelect = document.getElementById('user-type'); + if (userTypeSelect) { + userTypeSelect.value = 'user'; + } + + // Hide expiration group + const expirationGroup = document.getElementById('expiration-group'); + if (expirationGroup) { + expirationGroup.style.display = 'none'; + } + + // Re-enable admin checkbox + const isAdminCheckbox = document.getElementById('user-is-admin'); + if (isAdminCheckbox) { + isAdminCheckbox.disabled = false; + } + showStatus('User form cleared', 'info'); } } diff --git a/map/app/public/js/auth.js b/map/app/public/js/auth.js index 5241dde..e4e031d 100644 --- a/map/app/public/js/auth.js +++ b/map/app/public/js/auth.js @@ -8,24 +8,41 @@ export async function checkAuth() { const response = await fetch('/api/auth/check'); const data = await response.json(); + // Check if user has expired + if (data.expired) { + showStatus('Account has expired. Please contact an administrator.', 'error'); + setTimeout(() => { + window.location.href = '/login.html?expired=true'; + }, 2000); + throw new Error('Account expired'); + } + if (!data.authenticated) { window.location.href = '/login.html'; throw new Error('Not authenticated'); } currentUser = data.user; + currentUser.userType = data.user.userType || 'user'; // Ensure userType is set updateUserInterface(); } catch (error) { console.error('Auth check failed:', error); - window.location.href = '/login.html'; + if (error.message !== 'Account expired') { + window.location.href = '/login.html'; + } throw error; } } export function updateUserInterface() { if (!currentUser) return; - + + /* NEW – add a body class we can target with CSS */ + document.body.classList.toggle('temp-user', currentUser.userType === 'temp'); + document.body.classList.toggle('admin-user', currentUser.isAdmin === true); + + // ----- existing code that manipulates DOM ----- // Update user email in both desktop and mobile const userEmailElement = document.getElementById('user-email'); const mobileUserEmailElement = document.getElementById('mobile-user-email'); @@ -47,6 +64,52 @@ export function updateUserInterface() { } } + // Get all shifts links/buttons + const shiftsLinks = document.querySelectorAll('a[href="/shifts.html"]'); + + if (currentUser.userType === 'temp') { + // If user is temp, hide all shifts-related elements + shiftsLinks.forEach(link => { + const desktopButton = link.closest('.btn'); + const mobileItem = link.closest('.mobile-dropdown-item'); + + if (desktopButton) { + desktopButton.classList.add('temp-restricted'); + } + if (mobileItem) { + mobileItem.classList.add('temp-restricted'); + } + }); + } else { + // If user is NOT temp, ensure all shifts-related elements are visible + shiftsLinks.forEach(link => { + const desktopButton = link.closest('.btn'); + const mobileItem = link.closest('.mobile-dropdown-item'); + + if (desktopButton) { + desktopButton.classList.remove('temp-restricted'); + } + if (mobileItem) { + mobileItem.classList.remove('temp-restricted'); + } + }); + } + + // Add temp user indicator for temp users + if (currentUser.userType === 'temp') { + // Hide user profile links + const userLinks = document.querySelectorAll('a[href="/user.html"]'); + userLinks.forEach(link => link.style.display = 'none'); + + // Add temp user indicator + if (userEmailElement) { + userEmailElement.innerHTML = `${currentUser.email} Temp`; + } + if (mobileUserEmailElement) { + mobileUserEmailElement.innerHTML = `${currentUser.email} Temp`; + } + } + // Add admin link if user is admin if (currentUser.isAdmin) { addAdminLinks(); diff --git a/map/app/public/js/location-manager.js b/map/app/public/js/location-manager.js index dd4e33b..c4d1cea 100644 --- a/map/app/public/js/location-manager.js +++ b/map/app/public/js/location-manager.js @@ -244,10 +244,12 @@ function createPopupContent(location) { data-location='${escapeHtml(JSON.stringify(location))}'> ✏️ Edit - + ${currentUser.userType !== 'temp' ? ` + + ` : ''}
` : ''} @@ -345,6 +347,16 @@ export function openEditForm(location) { document.getElementById('edit-location-lng').value = location.longitude || ''; document.getElementById('edit-geo-location').value = location['Geo-Location'] || ''; + // Show/hide delete button based on user type + const deleteBtn = document.getElementById('delete-location-btn'); + if (deleteBtn) { + if (currentUser?.userType === 'temp') { + deleteBtn.style.display = 'none'; + } else { + deleteBtn.style.display = ''; + } + } + // Show edit footer document.getElementById('edit-footer').classList.remove('hidden'); } diff --git a/map/app/public/js/search-manager.js b/map/app/public/js/search-manager.js index f184ab7..0644f95 100644 --- a/map/app/public/js/search-manager.js +++ b/map/app/public/js/search-manager.js @@ -6,6 +6,7 @@ import { MkDocsSearch } from './mkdocs-search.js'; import mapSearch from './map-search.js'; import databaseSearch from './database-search.js'; +import { currentUser } from './auth.js'; export class UnifiedSearchManager { constructor(config = {}) { @@ -72,6 +73,14 @@ export class UnifiedSearchManager { return false; } + // Hide database search option for temp users + if (currentUser?.userType === 'temp') { + const databaseModeBtn = container.querySelector('[data-mode="database"]'); + if (databaseModeBtn) { + databaseModeBtn.style.display = 'none'; + } + } + this.setupEventListeners(); this.updatePlaceholder(); return true; @@ -187,6 +196,13 @@ export class UnifiedSearchManager { return; } + // Prevent database search for temp users + if (currentUser?.userType === 'temp' && mode === 'database') { + console.log('Database search not available for temporary users'); + this.showError('Database search is not available for temporary users'); + return; + } + this.mode = mode; // Update button states diff --git a/map/app/public/login.html b/map/app/public/login.html index 09caadc..ca8ba46 100644 --- a/map/app/public/login.html +++ b/map/app/public/login.html @@ -434,6 +434,14 @@ }) .catch(console.error); + // Check for expired parameter and show message + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('expired') === 'true') { + const errorMessage = document.getElementById('error-message'); + errorMessage.textContent = 'Your account has expired. Please contact an administrator.'; + errorMessage.classList.add('show'); + } + // Modal elements const modal = document.getElementById('password-recovery-modal'); const forgotPasswordLink = document.getElementById('forgot-password-link'); diff --git a/map/app/routes/index.js b/map/app/routes/index.js index 13cf496..c2f7dc9 100644 --- a/map/app/routes/index.js +++ b/map/app/routes/index.js @@ -1,6 +1,6 @@ const express = require('express'); const path = require('path'); -const { requireAuth, requireAdmin } = require('../middleware/auth'); +const { requireAuth, requireAdmin, requireNonTemp } = require('../middleware/auth'); // Import route modules const authRoutes = require('./auth'); @@ -53,7 +53,7 @@ module.exports = (app) => { app.use('/api/locations', requireAuth, locationRoutes); app.use('/api/geocode', requireAuth, geocodingRoutes); app.use('/api/settings', requireAuth, settingsRoutes); - app.use('/api/shifts', shiftsRoutes); + app.use('/api/shifts', requireNonTemp, shiftsRoutes); app.use('/api/external', externalDataRoutes); // Admin routes @@ -164,12 +164,12 @@ module.exports = (app) => { }); // Protected page route - app.get('/shifts.html', requireAuth, (req, res) => { + app.get('/shifts.html', requireNonTemp, (req, res) => { res.sendFile(path.join(__dirname, '../public', 'shifts.html')); }); // User profile page route - app.get('/user.html', requireAuth, (req, res) => { + app.get('/user.html', requireNonTemp, (req, res) => { res.sendFile(path.join(__dirname, '../public', 'user.html')); }); diff --git a/map/app/routes/locations.js b/map/app/routes/locations.js index bda627d..5e07dea 100644 --- a/map/app/routes/locations.js +++ b/map/app/routes/locations.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const locationsController = require('../controllers/locationsController'); const { strictLimiter } = require('../middleware/rateLimiter'); -const { requireAuth } = require('../middleware/auth'); +const { requireAuth, requireDeletePermission } = require('../middleware/auth'); // Get all locations (public) router.get('/', locationsController.getAll); @@ -16,7 +16,7 @@ router.post('/', requireAuth, strictLimiter, locationsController.create); // Update location (requires authentication) router.put('/:id', requireAuth, strictLimiter, locationsController.update); -// Delete location (requires authentication) -router.delete('/:id', requireAuth, strictLimiter, locationsController.delete); +// Delete location (requires authentication and delete permission) +router.delete('/:id', requireAuth, requireDeletePermission, strictLimiter, locationsController.delete); module.exports = router; \ No newline at end of file diff --git a/map/app/services/accountExpiration.js b/map/app/services/accountExpiration.js new file mode 100644 index 0000000..09b3c11 --- /dev/null +++ b/map/app/services/accountExpiration.js @@ -0,0 +1,88 @@ +const nocodbService = require('./nocodb'); +const logger = require('../utils/logger'); +const config = require('../config'); + +class AccountExpirationService { + constructor() { + this.checkInterval = null; + } + + // Start the expiration check service + start() { + // Run check every hour + this.checkInterval = setInterval(() => { + this.checkExpiredAccounts(); + }, 60 * 60 * 1000); // 1 hour + + // Also run immediately on start + this.checkExpiredAccounts(); + + logger.info('Account expiration service started'); + } + + // Stop the service + stop() { + if (this.checkInterval) { + clearInterval(this.checkInterval); + this.checkInterval = null; + } + } + + // Check and handle expired accounts + async checkExpiredAccounts() { + try { + // Get all users + const users = await nocodbService.getLoginRecords(); + + const now = new Date(); + const expiredUsers = []; + + for (const user of users) { + if (user.ExpiresAt && new Date(user.ExpiresAt) < now) { + expiredUsers.push(user); + } + } + + // Delete expired accounts + for (const user of expiredUsers) { + await this.deleteExpiredAccount(user); + } + + if (expiredUsers.length > 0) { + logger.info(`Deleted ${expiredUsers.length} expired temp accounts`); + } + + } catch (error) { + logger.error('Error checking expired accounts:', error); + } + } + + // Delete a single expired account + async deleteExpiredAccount(user) { + try { + await nocodbService.deleteRecord( + config.nocodb.loginSheetId, + user.Id + ); + + logger.info(`Deleted expired temp account: ${user.Email}`); + + // Optional: Send notification email before deletion + // await emailService.sendAccountExpirationNotice(user.Email); + + } catch (error) { + logger.error(`Failed to delete expired account ${user.Email}:`, error); + } + } + + // Calculate expiration date based on admin settings + static calculateExpirationDate(expireDays) { + if (!expireDays || expireDays <= 0) return null; + + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + expireDays); + return expirationDate; + } +} + +module.exports = new AccountExpirationService();