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 @@
+
+
+
+
+
+
+
+ Account will auto-delete after this many days
+
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();
|