New login view
This commit is contained in:
parent
91c651da48
commit
fa64722020
@ -3,6 +3,13 @@ NOCODB_API_URL=https://db.lindalindsay.org/api/v1
|
||||
NOCODB_API_TOKEN=your-api-token-here
|
||||
NOCODB_VIEW_URL=https://db.lindalindsay.org/dashboard/#/nc/p406kno3lbq4zmq/mvtryxrvze6td79
|
||||
|
||||
# Login Sheet Configuration
|
||||
# You can use either the full URL (recommended) or just the table ID
|
||||
# Full URL example:
|
||||
NOCODB_LOGIN_SHEET=https://db.lindalindsay.org/dashboard/#/nc/p406kno3lbq4zmq/mnlzyenzfg1v1mo
|
||||
# Or just the table ID:
|
||||
# NOCODB_LOGIN_SHEET=mnlzyenzfg1v1mo
|
||||
|
||||
# Auto-parsed from NOCODB_VIEW_URL (no need to set manually)
|
||||
# NOCODB_PROJECT_ID=p406kno3lbq4zmq
|
||||
# NOCODB_TABLE_ID=mvtryxrvze6td79
|
||||
@ -11,6 +18,10 @@ NOCODB_VIEW_URL=https://db.lindalindsay.org/dashboard/#/nc/p406kno3lbq4zmq/mvtry
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# Session Secret (IMPORTANT: Generate a secure random string for production)
|
||||
# You can generate one with: openssl rand -hex 32
|
||||
SESSION_SECRET=your-secure-session-secret-here
|
||||
|
||||
# Map Defaults (Edmonton, Alberta, Canada)
|
||||
DEFAULT_LAT=53.5461
|
||||
DEFAULT_LNG=-113.4938
|
||||
|
||||
88
nocodb-map-viewer/app/package-lock.json
generated
88
nocodb-map-viewer/app/package-lock.json
generated
@ -10,10 +10,12 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.4",
|
||||
"express-session": "^1.18.1",
|
||||
"helmet": "^7.1.0",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
@ -331,6 +333,28 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
@ -555,6 +579,40 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
|
||||
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.7",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-headers": "~1.0.2",
|
||||
"parseurl": "~1.3.3",
|
||||
"safe-buffer": "5.2.1",
|
||||
"uid-safe": "~2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express-session/node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||
@ -1133,6 +1191,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
@ -1211,6 +1278,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@ -1564,6 +1640,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
|
||||
@ -18,12 +18,14 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"axios": "^1.6.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"helmet": "^7.1.0",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.4",
|
||||
"express-session": "^1.18.1",
|
||||
"helmet": "^7.1.0",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -66,6 +66,21 @@ body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* User info in header */
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 0 15px;
|
||||
border-right: 1px solid rgba(255,255,255,0.2);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
/* Map container */
|
||||
#map-container {
|
||||
flex: 1;
|
||||
@ -555,6 +570,11 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide user info on mobile to save space */
|
||||
.user-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px;
|
||||
min-width: 40px;
|
||||
|
||||
@ -14,11 +14,13 @@ let markers = [];
|
||||
let userLocationMarker = null;
|
||||
let isAddingLocation = false;
|
||||
let refreshInterval = null;
|
||||
let currentEditingLocation = null; // Add this line
|
||||
let currentEditingLocation = null;
|
||||
let currentUser = null; // Add current user state
|
||||
|
||||
// Initialize application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeMap();
|
||||
checkAuthentication(); // Add authentication check
|
||||
loadLocations();
|
||||
setupEventListeners();
|
||||
checkConfiguration();
|
||||
@ -222,6 +224,68 @@ function setupGeoFieldSync() {
|
||||
});
|
||||
}
|
||||
|
||||
// Check authentication and display user info
|
||||
async function checkAuthentication() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/check');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
currentUser = data.user;
|
||||
displayUserInfo();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check authentication:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Display user info in header
|
||||
function displayUserInfo() {
|
||||
const headerActions = document.querySelector('.header-actions');
|
||||
|
||||
// Create user info element
|
||||
const userInfo = document.createElement('div');
|
||||
userInfo.className = 'user-info';
|
||||
userInfo.innerHTML = `
|
||||
<span class="user-email">${escapeHtml(currentUser.email)}</span>
|
||||
<button id="logout-btn" class="btn btn-secondary btn-sm" title="Sign out">
|
||||
🚪 Logout
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Insert before the location count
|
||||
const locationCount = document.getElementById('location-count');
|
||||
headerActions.insertBefore(userInfo, locationCount);
|
||||
|
||||
// Add logout event listener
|
||||
document.getElementById('logout-btn').addEventListener('click', handleLogout);
|
||||
}
|
||||
|
||||
// Handle logout
|
||||
async function handleLogout() {
|
||||
if (!confirm('Are you sure you want to logout?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.href = '/login.html';
|
||||
} else {
|
||||
showStatus('Logout failed. Please try again.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
showStatus('Logout failed. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Check API configuration
|
||||
async function checkConfiguration() {
|
||||
try {
|
||||
|
||||
240
nocodb-map-viewer/app/public/login.html
Normal file
240
nocodb-map-viewer/app/public/login.html
Normal file
@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Login to NocoDB Map Viewer">
|
||||
<title>Login - NocoDB Map Viewer</title>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--light-color);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: var(--dark-color);
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: var(--secondary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(44, 90, 160, 0.1);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background-color: #2471a3;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
background-color: var(--secondary-color);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #fee;
|
||||
color: var(--danger-color);
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: #efe;
|
||||
color: var(--success-color);
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
color: var(--secondary-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h1>Location Map Viewer</h1>
|
||||
<p>Please sign in to continue</p>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="error-message"></div>
|
||||
<div id="success-message" class="success-message"></div>
|
||||
|
||||
<form id="login-form" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Enter your email address"
|
||||
required
|
||||
autocomplete="email"
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button" id="login-button">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>Access is restricted to authorized users only.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Handle login form submission
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const button = document.getElementById('login-button');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const successMessage = document.getElementById('success-message');
|
||||
|
||||
// Clear previous messages
|
||||
errorMessage.classList.remove('show');
|
||||
successMessage.classList.remove('show');
|
||||
|
||||
// Disable button and show loading state
|
||||
button.disabled = true;
|
||||
button.textContent = 'Signing in...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
successMessage.textContent = 'Login successful! Redirecting...';
|
||||
successMessage.classList.add('show');
|
||||
|
||||
// Redirect to main app after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
errorMessage.textContent = data.error || 'Invalid email address';
|
||||
errorMessage.classList.add('show');
|
||||
button.disabled = false;
|
||||
button.textContent = 'Sign In';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
errorMessage.textContent = 'An error occurred. Please try again.';
|
||||
errorMessage.classList.add('show');
|
||||
button.disabled = false;
|
||||
button.textContent = 'Sign In';
|
||||
}
|
||||
});
|
||||
|
||||
// Check if already logged in
|
||||
fetch('/api/auth/check')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.authenticated) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -5,6 +5,8 @@ const helmet = require('helmet');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
const session = require('express-session');
|
||||
const cookieParser = require('cookie-parser');
|
||||
require('dotenv').config();
|
||||
|
||||
// Import geocoding routes
|
||||
@ -92,6 +94,25 @@ if (process.env.NOCODB_VIEW_URL && (!process.env.NOCODB_PROJECT_ID || !process.e
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-parse login sheet ID if URL is provided
|
||||
let LOGIN_SHEET_ID = null;
|
||||
if (process.env.NOCODB_LOGIN_SHEET) {
|
||||
// Check if it's a URL or just an ID
|
||||
if (process.env.NOCODB_LOGIN_SHEET.startsWith('http')) {
|
||||
const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_LOGIN_SHEET);
|
||||
if (projectId && tableId) {
|
||||
LOGIN_SHEET_ID = tableId;
|
||||
console.log(`Auto-parsed login sheet ID from URL: ${LOGIN_SHEET_ID}`);
|
||||
} else {
|
||||
console.error('Could not parse login sheet URL');
|
||||
}
|
||||
} else {
|
||||
// Assume it's already just the ID
|
||||
LOGIN_SHEET_ID = process.env.NOCODB_LOGIN_SHEET;
|
||||
console.log(`Using login sheet ID: ${LOGIN_SHEET_ID}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
@ -110,6 +131,19 @@ const logger = winston.createLogger({
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Session configuration
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
@ -140,15 +174,161 @@ const strictLimiter = rateLimit({
|
||||
max: 20 // limit location creation to 20 per 15 minutes
|
||||
});
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5 // limit login attempts to 5 per 15 minutes
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session && req.session.authenticated) {
|
||||
next();
|
||||
} else {
|
||||
if (req.xhr || req.headers.accept?.indexOf('json') > -1) {
|
||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||
} else {
|
||||
res.redirect('/login.html');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Serve login page without authentication
|
||||
app.get('/login.html', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'login.html'));
|
||||
});
|
||||
|
||||
// Auth routes (no authentication required)
|
||||
app.post('/api/auth/login', authLimiter, async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Email is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid email format'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if login sheet is configured
|
||||
if (!LOGIN_SHEET_ID) {
|
||||
logger.error('NOCODB_LOGIN_SHEET not configured or could not be parsed');
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication system not properly configured'
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch authorized emails from NocoDB
|
||||
const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`;
|
||||
|
||||
logger.info(`Checking authentication for email: ${email}`);
|
||||
logger.debug(`Using login sheet API: ${url}`);
|
||||
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
'xc-token': process.env.NOCODB_API_TOKEN,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
params: {
|
||||
limit: 1000 // Adjust if you have more authorized users
|
||||
}
|
||||
});
|
||||
|
||||
const users = response.data.list || [];
|
||||
|
||||
// Check if email exists in the authorized users list
|
||||
const authorizedUser = users.find(user =>
|
||||
user.Email && user.Email.toLowerCase() === email.toLowerCase()
|
||||
);
|
||||
|
||||
if (authorizedUser) {
|
||||
// Set session
|
||||
req.session.authenticated = true;
|
||||
req.session.userEmail = email;
|
||||
req.session.userName = authorizedUser.Name || email;
|
||||
|
||||
logger.info(`User authenticated: ${email}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
email: email,
|
||||
name: req.session.userName
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.warn(`Authentication failed for email: ${email}`);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Email not authorized. Please contact an administrator.'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Login error:', error.message);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication service error. Please try again later.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/auth/check', (req, res) => {
|
||||
res.json({
|
||||
authenticated: req.session?.authenticated || false,
|
||||
user: req.session?.authenticated ? {
|
||||
email: req.session.userEmail,
|
||||
name: req.session.userName
|
||||
} : null
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
logger.error('Logout error:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Logout failed'
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logged out successfully'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Serve static files with authentication for main app
|
||||
app.use(express.static(path.join(__dirname, 'public'), {
|
||||
index: false // Don't serve index.html automatically
|
||||
}));
|
||||
|
||||
// Protect main app routes
|
||||
app.get('/', requireAuth, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// Add geocoding routes (protected)
|
||||
app.use('/api/geocode', requireAuth, geocodingRoutes);
|
||||
|
||||
// Apply rate limiting to API routes
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Add geocoding routes
|
||||
app.use('/api/geocode', geocodingRoutes);
|
||||
|
||||
// Health check endpoint
|
||||
// Health check endpoint (no auth required)
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
@ -157,15 +337,18 @@ app.get('/health', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Configuration validation endpoint (for debugging)
|
||||
app.get('/api/config-check', (req, res) => {
|
||||
// Configuration validation endpoint (protected)
|
||||
app.get('/api/config-check', requireAuth, (req, res) => {
|
||||
const config = {
|
||||
hasApiUrl: !!process.env.NOCODB_API_URL,
|
||||
hasApiToken: !!process.env.NOCODB_API_TOKEN,
|
||||
hasProjectId: !!process.env.NOCODB_PROJECT_ID,
|
||||
hasTableId: !!process.env.NOCODB_TABLE_ID,
|
||||
hasLoginSheet: !!LOGIN_SHEET_ID,
|
||||
projectId: process.env.NOCODB_PROJECT_ID,
|
||||
tableId: process.env.NOCODB_TABLE_ID,
|
||||
loginSheet: LOGIN_SHEET_ID,
|
||||
loginSheetConfigured: process.env.NOCODB_LOGIN_SHEET,
|
||||
nodeEnv: process.env.NODE_ENV
|
||||
};
|
||||
|
||||
@ -177,6 +360,9 @@ app.get('/api/config-check', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// All other API routes require authentication
|
||||
app.use('/api/*', requireAuth);
|
||||
|
||||
// Get all locations from NocoDB
|
||||
app.get('/api/locations', async (req, res) => {
|
||||
try {
|
||||
@ -380,7 +566,8 @@ app.post('/api/locations', strictLimiter, async (req, res) => {
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
...additionalData,
|
||||
created_at: new Date().toISOString()
|
||||
created_at: new Date().toISOString(),
|
||||
created_by: req.session.userEmail // Track who created the location
|
||||
};
|
||||
|
||||
const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`;
|
||||
@ -428,6 +615,7 @@ app.put('/api/locations/:id', strictLimiter, async (req, res) => {
|
||||
updateData = syncGeoFields(updateData);
|
||||
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
updateData.updated_by = req.session.userEmail; // Track who updated
|
||||
|
||||
const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${req.params.id}`;
|
||||
|
||||
@ -463,7 +651,7 @@ app.delete('/api/locations/:id', strictLimiter, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`Location ${req.params.id} deleted`);
|
||||
logger.info(`Location ${req.params.id} deleted by ${req.session.userEmail}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@ -499,6 +687,7 @@ app.listen(PORT, () => {
|
||||
║ Environment: ${process.env.NODE_ENV || 'development'} ║
|
||||
║ Project ID: ${process.env.NOCODB_PROJECT_ID} ║
|
||||
║ Table ID: ${process.env.NOCODB_TABLE_ID} ║
|
||||
║ Login Sheet: ${LOGIN_SHEET_ID || 'Not Configured'} ║
|
||||
║ Time: ${new Date().toISOString()} ║
|
||||
╚════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user