Temp user updates and several bug fixes
This commit is contained in:
parent
2a53008e04
commit
2ebbb2dc44
115
map/TEMP_USER_IMPLEMENTATION.md
Normal file
115
map/TEMP_USER_IMPLEMENTATION.md
Normal 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
146
map/TEMP_USER_TEST.md
Normal 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.
|
||||||
@ -50,7 +50,25 @@ class AuthController {
|
|||||||
error: 'Invalid email or password'
|
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
|
// Update last login time
|
||||||
try {
|
try {
|
||||||
const userId = extractId(user);
|
const userId = extractId(user);
|
||||||
@ -73,6 +91,7 @@ class AuthController {
|
|||||||
req.session.userEmail = user.email || user.Email; // Make sure this is set
|
req.session.userEmail = user.email || user.Email; // Make sure this is set
|
||||||
req.session.userName = user.name || user.Name;
|
req.session.userName = user.name || user.Name;
|
||||||
req.session.isAdmin = user.admin || user.Admin || false;
|
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:', {
|
logger.info('User logged in:', {
|
||||||
email: req.session.userEmail,
|
email: req.session.userEmail,
|
||||||
@ -97,7 +116,8 @@ class AuthController {
|
|||||||
user: {
|
user: {
|
||||||
email: email,
|
email: email,
|
||||||
name: req.session.userName,
|
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) {
|
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({
|
res.json({
|
||||||
authenticated: req.session?.authenticated || false,
|
authenticated: req.session?.authenticated || false,
|
||||||
user: req.session?.authenticated ? {
|
user: req.session?.authenticated ? {
|
||||||
email: req.session.userEmail,
|
email: req.session.userEmail,
|
||||||
name: req.session.userName,
|
name: req.session.userName,
|
||||||
isAdmin: req.session.isAdmin || false
|
isAdmin: req.session.isAdmin || false,
|
||||||
|
userType: req.session.userType || 'user'
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class UsersController {
|
|||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
try {
|
||||||
const { email, password, name, admin } = req.body;
|
const { email, password, name, isAdmin, userType, expireDays } = req.body;
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
return res.status(400).json({
|
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
|
// Create new user - use the actual column names from your table
|
||||||
const userData = {
|
const userData = {
|
||||||
Email: email,
|
Email: email,
|
||||||
@ -89,9 +97,13 @@ class UsersController {
|
|||||||
password: password,
|
password: password,
|
||||||
Name: name || '',
|
Name: name || '',
|
||||||
name: name || '',
|
name: name || '',
|
||||||
Admin: admin === true,
|
Admin: isAdmin === true,
|
||||||
admin: admin === true
|
admin: isAdmin === true,
|
||||||
// Removed created_at fields as they might not exist
|
UserType: userType || 'user',
|
||||||
|
userType: userType || 'user',
|
||||||
|
CreatedAt: new Date().toISOString(),
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
ExpireDays: userType === 'temp' ? expireDays : null
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await nocodbService.create(
|
const response = await nocodbService.create(
|
||||||
@ -106,7 +118,9 @@ class UsersController {
|
|||||||
id: extractId(response),
|
id: extractId(response),
|
||||||
email: email,
|
email: email,
|
||||||
name: name,
|
name: name,
|
||||||
admin: admin
|
admin: isAdmin,
|
||||||
|
userType: userType,
|
||||||
|
expiresAt: expiresAt
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,59 @@
|
|||||||
const logger = require('../utils/logger');
|
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
|
// Check for both authentication patterns used in your app
|
||||||
const isAuthenticated = (req.session && req.session.authenticated) ||
|
const isAuthenticated = (req.session && req.session.authenticated) ||
|
||||||
(req.session && req.session.userId && req.session.userEmail);
|
(req.session && req.session.userId && req.session.userEmail);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
|
// Check if temp user has expired
|
||||||
|
const expirationResponse = await checkTempUserExpiration(req, res);
|
||||||
|
if (expirationResponse) {
|
||||||
|
return; // Response already sent by checkTempUserExpiration
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Unauthorized access attempt', {
|
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
|
// Check for both authentication patterns used in your app
|
||||||
const isAuthenticated = (req.session && req.session.authenticated) ||
|
const isAuthenticated = (req.session && req.session.authenticated) ||
|
||||||
(req.session && req.session.userId && req.session.userEmail);
|
(req.session && req.session.userId && req.session.userEmail);
|
||||||
|
|
||||||
if (isAuthenticated && req.session.isAdmin) {
|
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();
|
next();
|
||||||
} else {
|
} else {
|
||||||
logger.warn('Unauthorized admin access attempt', {
|
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 = {
|
module.exports = {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireAdmin
|
requireAdmin,
|
||||||
|
requireNonTemp,
|
||||||
|
requireDeletePermission
|
||||||
};
|
};
|
||||||
@ -422,10 +422,23 @@
|
|||||||
<label for="user-password">Password</label>
|
<label for="user-password">Password</label>
|
||||||
<input type="password" id="user-password" required>
|
<input type="password" id="user-password" required>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="user-is-admin">
|
<input type="checkbox" id="user-is-admin">
|
||||||
Is Admin
|
Is Admin (Legacy - use User Type instead)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@ -815,6 +815,44 @@
|
|||||||
color: white;
|
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 {
|
.user-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@ -54,3 +54,22 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: hidden; /* Prevent horizontal scrolling */
|
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;
|
||||||
|
}
|
||||||
|
|||||||
31
map/app/public/css/modules/temp-user.css
Normal file
31
map/app/public/css/modules/temp-user.css
Normal 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;
|
||||||
|
}
|
||||||
@ -16,3 +16,4 @@
|
|||||||
@import url("modules/cache-busting.css");
|
@import url("modules/cache-busting.css");
|
||||||
@import url("modules/apartment-popup.css");
|
@import url("modules/apartment-popup.css");
|
||||||
@import url("modules/apartment-marker.css");
|
@import url("modules/apartment-marker.css");
|
||||||
|
@import url("modules/temp-user.css")
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
<span class="btn-text">Homepage</span>
|
<span class="btn-text">Homepage</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="/shifts.html" class="btn btn-secondary">
|
<a href="/shifts.html" class="btn btn-secondary">
|
||||||
<span class="btn-icon">📅</span>
|
<span class="btn-icon">📅</span>
|
||||||
<span class="btn-text">View Shifts</span>
|
<span class="btn-text">View Shifts</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -358,6 +358,30 @@ function setupEventListeners() {
|
|||||||
if (clearUserBtn) {
|
if (clearUserBtn) {
|
||||||
clearUserBtn.addEventListener('click', clearUserForm);
|
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
|
// Setup navigation between admin sections
|
||||||
@ -1282,16 +1306,34 @@ function displayUsers(users) {
|
|||||||
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
const createdDate = user.created_at || user['Created At'] || user.createdAt;
|
||||||
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
const formattedDate = createdDate ? new Date(createdDate).toLocaleDateString() : 'N/A';
|
||||||
const isAdmin = user.admin || user.Admin || false;
|
const isAdmin = user.admin || user.Admin || false;
|
||||||
|
const userType = user.UserType || user.userType || (isAdmin ? 'admin' : 'user');
|
||||||
const userId = user.Id || user.id || user.ID;
|
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 `
|
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="Email">${escapeHtml(user.email || user.Email || 'N/A')}</td>
|
||||||
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
|
<td data-label="Name">${escapeHtml(user.name || user.Name || 'N/A')}</td>
|
||||||
<td data-label="Role">
|
<td data-label="Role">
|
||||||
<span class="user-role ${isAdmin ? 'admin' : 'user'}">
|
<span class="user-role ${userType}">
|
||||||
${isAdmin ? 'Admin' : 'User'}
|
${userType.charAt(0).toUpperCase() + userType.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
|
${expirationInfo}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Created">${formattedDate}</td>
|
<td data-label="Created">${formattedDate}</td>
|
||||||
<td data-label="Actions">
|
<td data-label="Actions">
|
||||||
@ -1401,6 +1443,9 @@ async function createUser(e) {
|
|||||||
const email = document.getElementById('user-email').value.trim();
|
const email = document.getElementById('user-email').value.trim();
|
||||||
const password = document.getElementById('user-password').value;
|
const password = document.getElementById('user-password').value;
|
||||||
const name = document.getElementById('user-name').value.trim();
|
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;
|
const admin = document.getElementById('user-is-admin').checked;
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
@ -1413,18 +1458,27 @@ async function createUser(e) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userType === 'temp' && (!expireDays || expireDays < 1 || expireDays > 365)) {
|
||||||
|
showStatus('Expiration days must be between 1 and 365 for temporary users', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const userData = {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name: name || '',
|
||||||
|
isAdmin: userType === 'admin' || admin,
|
||||||
|
userType,
|
||||||
|
expireDays
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch('/api/users', {
|
const response = await fetch('/api/users', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(userData)
|
||||||
email,
|
|
||||||
password,
|
|
||||||
name: name || '',
|
|
||||||
admin
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@ -1447,6 +1501,25 @@ function clearUserForm() {
|
|||||||
const form = document.getElementById('create-user-form');
|
const form = document.getElementById('create-user-form');
|
||||||
if (form) {
|
if (form) {
|
||||||
form.reset();
|
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');
|
showStatus('User form cleared', 'info');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,24 +8,41 @@ export async function checkAuth() {
|
|||||||
const response = await fetch('/api/auth/check');
|
const response = await fetch('/api/auth/check');
|
||||||
const data = await response.json();
|
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) {
|
if (!data.authenticated) {
|
||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
throw new Error('Not authenticated');
|
throw new Error('Not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
currentUser = data.user;
|
currentUser = data.user;
|
||||||
|
currentUser.userType = data.user.userType || 'user'; // Ensure userType is set
|
||||||
updateUserInterface();
|
updateUserInterface();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check failed:', error);
|
console.error('Auth check failed:', error);
|
||||||
window.location.href = '/login.html';
|
if (error.message !== 'Account expired') {
|
||||||
|
window.location.href = '/login.html';
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUserInterface() {
|
export function updateUserInterface() {
|
||||||
if (!currentUser) return;
|
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
|
// Update user email in both desktop and mobile
|
||||||
const userEmailElement = document.getElementById('user-email');
|
const userEmailElement = document.getElementById('user-email');
|
||||||
const mobileUserEmailElement = document.getElementById('mobile-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
|
// Add admin link if user is admin
|
||||||
if (currentUser.isAdmin) {
|
if (currentUser.isAdmin) {
|
||||||
addAdminLinks();
|
addAdminLinks();
|
||||||
|
|||||||
@ -244,10 +244,12 @@ function createPopupContent(location) {
|
|||||||
data-location='${escapeHtml(JSON.stringify(location))}'>
|
data-location='${escapeHtml(JSON.stringify(location))}'>
|
||||||
✏️ Edit
|
✏️ Edit
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary btn-sm move-location-popup-btn"
|
${currentUser.userType !== 'temp' ? `
|
||||||
data-location='${escapeHtml(JSON.stringify(location))}'>
|
<button class="btn btn-primary btn-sm move-location-popup-btn"
|
||||||
📍 Move
|
data-location='${escapeHtml(JSON.stringify(location))}'>
|
||||||
</button>
|
📍 Move
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@ -345,6 +347,16 @@ export function openEditForm(location) {
|
|||||||
document.getElementById('edit-location-lng').value = location.longitude || '';
|
document.getElementById('edit-location-lng').value = location.longitude || '';
|
||||||
document.getElementById('edit-geo-location').value = location['Geo-Location'] || '';
|
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
|
// Show edit footer
|
||||||
document.getElementById('edit-footer').classList.remove('hidden');
|
document.getElementById('edit-footer').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
import { MkDocsSearch } from './mkdocs-search.js';
|
import { MkDocsSearch } from './mkdocs-search.js';
|
||||||
import mapSearch from './map-search.js';
|
import mapSearch from './map-search.js';
|
||||||
import databaseSearch from './database-search.js';
|
import databaseSearch from './database-search.js';
|
||||||
|
import { currentUser } from './auth.js';
|
||||||
|
|
||||||
export class UnifiedSearchManager {
|
export class UnifiedSearchManager {
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
@ -72,6 +73,14 @@ export class UnifiedSearchManager {
|
|||||||
return false;
|
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.setupEventListeners();
|
||||||
this.updatePlaceholder();
|
this.updatePlaceholder();
|
||||||
return true;
|
return true;
|
||||||
@ -187,6 +196,13 @@ export class UnifiedSearchManager {
|
|||||||
return;
|
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;
|
this.mode = mode;
|
||||||
|
|
||||||
// Update button states
|
// Update button states
|
||||||
|
|||||||
@ -434,6 +434,14 @@
|
|||||||
})
|
})
|
||||||
.catch(console.error);
|
.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
|
// Modal elements
|
||||||
const modal = document.getElementById('password-recovery-modal');
|
const modal = document.getElementById('password-recovery-modal');
|
||||||
const forgotPasswordLink = document.getElementById('forgot-password-link');
|
const forgotPasswordLink = document.getElementById('forgot-password-link');
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
const { requireAuth, requireAdmin, requireNonTemp } = require('../middleware/auth');
|
||||||
|
|
||||||
// Import route modules
|
// Import route modules
|
||||||
const authRoutes = require('./auth');
|
const authRoutes = require('./auth');
|
||||||
@ -53,7 +53,7 @@ module.exports = (app) => {
|
|||||||
app.use('/api/locations', requireAuth, locationRoutes);
|
app.use('/api/locations', requireAuth, locationRoutes);
|
||||||
app.use('/api/geocode', requireAuth, geocodingRoutes);
|
app.use('/api/geocode', requireAuth, geocodingRoutes);
|
||||||
app.use('/api/settings', requireAuth, settingsRoutes);
|
app.use('/api/settings', requireAuth, settingsRoutes);
|
||||||
app.use('/api/shifts', shiftsRoutes);
|
app.use('/api/shifts', requireNonTemp, shiftsRoutes);
|
||||||
app.use('/api/external', externalDataRoutes);
|
app.use('/api/external', externalDataRoutes);
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
@ -164,12 +164,12 @@ module.exports = (app) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Protected page route
|
// 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'));
|
res.sendFile(path.join(__dirname, '../public', 'shifts.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
// User profile page route
|
// 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'));
|
res.sendFile(path.join(__dirname, '../public', 'user.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const locationsController = require('../controllers/locationsController');
|
const locationsController = require('../controllers/locationsController');
|
||||||
const { strictLimiter } = require('../middleware/rateLimiter');
|
const { strictLimiter } = require('../middleware/rateLimiter');
|
||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth, requireDeletePermission } = require('../middleware/auth');
|
||||||
|
|
||||||
// Get all locations (public)
|
// Get all locations (public)
|
||||||
router.get('/', locationsController.getAll);
|
router.get('/', locationsController.getAll);
|
||||||
@ -16,7 +16,7 @@ router.post('/', requireAuth, strictLimiter, locationsController.create);
|
|||||||
// Update location (requires authentication)
|
// Update location (requires authentication)
|
||||||
router.put('/:id', requireAuth, strictLimiter, locationsController.update);
|
router.put('/:id', requireAuth, strictLimiter, locationsController.update);
|
||||||
|
|
||||||
// Delete location (requires authentication)
|
// Delete location (requires authentication and delete permission)
|
||||||
router.delete('/:id', requireAuth, strictLimiter, locationsController.delete);
|
router.delete('/:id', requireAuth, requireDeletePermission, strictLimiter, locationsController.delete);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
88
map/app/services/accountExpiration.js
Normal file
88
map/app/services/accountExpiration.js
Normal 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();
|
||||||
Loading…
x
Reference in New Issue
Block a user