Temp user updates and several bug fixes

This commit is contained in:
admin 2025-08-03 16:08:11 -06:00
parent 2a53008e04
commit 2ebbb2dc44
19 changed files with 843 additions and 35 deletions

View File

@ -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

146
map/TEMP_USER_TEST.md Normal file
View File

@ -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.

View File

@ -51,6 +51,24 @@ class AuthController {
});
}
// 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
});
}

View File

@ -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
}
});

View File

@ -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
};

View File

@ -422,10 +422,23 @@
<label for="user-password">Password</label>
<input type="password" id="user-password" required>
</div>
<div class="form-group">
<label for="user-type">User Type</label>
<select id="user-type" required>
<option value="user">Regular User</option>
<option value="temp">Temporary User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="expiration-group" style="display: none;">
<label for="user-expire-days">Expires After (days)</label>
<input type="number" id="user-expire-days" min="1" max="365" value="30">
<small class="help-text">Account will auto-delete after this many days</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="user-is-admin">
Is Admin
Is Admin (Legacy - use User Type instead)
</label>
</div>
<div class="form-actions">

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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")

View File

@ -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 = `<span class="expiration-info expiration-warning">Expired ${Math.abs(daysUntilExpiration)} days ago</span>`;
} else if (daysUntilExpiration <= 3) {
expirationInfo = `<span class="expiration-info expiration-warning">Expires in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? 's' : ''}</span>`;
} else {
expirationInfo = `<span class="expiration-info">Expires: ${expirationDate.toLocaleDateString()}</span>`;
}
}
return `
<tr>
<tr ${user.ExpiresAt && new Date(user.ExpiresAt) < new Date() ? 'class="expired"' : (user.ExpiresAt && new Date(user.ExpiresAt) - new Date() < 3 * 24 * 60 * 60 * 1000 ? 'class="expires-soon"' : '')}>
<td data-label="Email">${escapeHtml(user.email || user.Email || 'N/A')}</td>
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
<td data-label="Role">
<span class="user-role ${isAdmin ? 'admin' : 'user'}">
${isAdmin ? 'Admin' : 'User'}
<span class="user-role ${userType}">
${userType.charAt(0).toUpperCase() + userType.slice(1)}
</span>
${expirationInfo}
</td>
<td data-label="Created">${formattedDate}</td>
<td data-label="Actions">
@ -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');
}
}

View File

@ -8,17 +8,29 @@ 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);
if (error.message !== 'Account expired') {
window.location.href = '/login.html';
}
throw error;
}
}
@ -26,6 +38,11 @@ export async function checkAuth() {
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} <span class="badge temp-badge">Temp</span>`;
}
if (mobileUserEmailElement) {
mobileUserEmailElement.innerHTML = `${currentUser.email} <span class="badge temp-badge">Temp</span>`;
}
}
// Add admin link if user is admin
if (currentUser.isAdmin) {
addAdminLinks();

View File

@ -244,10 +244,12 @@ function createPopupContent(location) {
data-location='${escapeHtml(JSON.stringify(location))}'>
Edit
</button>
${currentUser.userType !== 'temp' ? `
<button class="btn btn-primary btn-sm move-location-popup-btn"
data-location='${escapeHtml(JSON.stringify(location))}'>
📍 Move
</button>
` : ''}
</div>
` : ''}
</div>
@ -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');
}

View File

@ -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

View File

@ -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');

View File

@ -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'));
});

View File

@ -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;

View File

@ -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();