From e7269e808fb106cc15032123ab5a9696e5568243 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 26 Jun 2025 11:45:24 -0600 Subject: [PATCH] Map updates --- {nocodb-map-viewer => map}/.gitignore | 0 {nocodb-map-viewer => map}/README.md | 0 {nocodb-map-viewer => map}/app/Dockerfile | 0 .../app/package-lock.json | 88 ++++++ {nocodb-map-viewer => map}/app/package.json | 6 +- .../app/public/css/style.css | 20 ++ .../app/public/index.html | 0 .../app/public/js/map.js | 66 +++- map/app/public/login.html | 254 +++++++++++++++ .../app/routes/geocoding.js | 0 {nocodb-map-viewer => map}/app/server.js | 288 +++++++++++++++++- .../app/services/geocoding.js | 0 {nocodb-map-viewer => map}/docker-compose.yml | 0 13 files changed, 704 insertions(+), 18 deletions(-) rename {nocodb-map-viewer => map}/.gitignore (100%) rename {nocodb-map-viewer => map}/README.md (100%) rename {nocodb-map-viewer => map}/app/Dockerfile (100%) rename {nocodb-map-viewer => map}/app/package-lock.json (94%) rename {nocodb-map-viewer => map}/app/package.json (91%) rename {nocodb-map-viewer => map}/app/public/css/style.css (96%) rename {nocodb-map-viewer => map}/app/public/index.html (100%) rename {nocodb-map-viewer => map}/app/public/js/map.js (94%) create mode 100644 map/app/public/login.html rename {nocodb-map-viewer => map}/app/routes/geocoding.js (100%) rename {nocodb-map-viewer => map}/app/server.js (64%) rename {nocodb-map-viewer => map}/app/services/geocoding.js (100%) rename {nocodb-map-viewer => map}/docker-compose.yml (100%) diff --git a/nocodb-map-viewer/.gitignore b/map/.gitignore similarity index 100% rename from nocodb-map-viewer/.gitignore rename to map/.gitignore diff --git a/nocodb-map-viewer/README.md b/map/README.md similarity index 100% rename from nocodb-map-viewer/README.md rename to map/README.md diff --git a/nocodb-map-viewer/app/Dockerfile b/map/app/Dockerfile similarity index 100% rename from nocodb-map-viewer/app/Dockerfile rename to map/app/Dockerfile diff --git a/nocodb-map-viewer/app/package-lock.json b/map/app/package-lock.json similarity index 94% rename from nocodb-map-viewer/app/package-lock.json rename to map/app/package-lock.json index 65aa5e5..4e2f404 100644 --- a/nocodb-map-viewer/app/package-lock.json +++ b/map/app/package-lock.json @@ -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", diff --git a/nocodb-map-viewer/app/package.json b/map/app/package.json similarity index 91% rename from nocodb-map-viewer/app/package.json rename to map/app/package.json index 5cc4a14..7ac5132 100644 --- a/nocodb-map-viewer/app/package.json +++ b/map/app/package.json @@ -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": { diff --git a/nocodb-map-viewer/app/public/css/style.css b/map/app/public/css/style.css similarity index 96% rename from nocodb-map-viewer/app/public/css/style.css rename to map/app/public/css/style.css index 0e6c77b..99f4755 100644 --- a/nocodb-map-viewer/app/public/css/style.css +++ b/map/app/public/css/style.css @@ -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; diff --git a/nocodb-map-viewer/app/public/index.html b/map/app/public/index.html similarity index 100% rename from nocodb-map-viewer/app/public/index.html rename to map/app/public/index.html diff --git a/nocodb-map-viewer/app/public/js/map.js b/map/app/public/js/map.js similarity index 94% rename from nocodb-map-viewer/app/public/js/map.js rename to map/app/public/js/map.js index 7ed4912..7460317 100644 --- a/nocodb-map-viewer/app/public/js/map.js +++ b/map/app/public/js/map.js @@ -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 = ` + ${escapeHtml(currentUser.email)} + + `; + + // 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 { diff --git a/map/app/public/login.html b/map/app/public/login.html new file mode 100644 index 0000000..2daa46c --- /dev/null +++ b/map/app/public/login.html @@ -0,0 +1,254 @@ + + + + + + + Login - NocoDB Map Viewer + + + + + + +
+ +
+ + + + diff --git a/nocodb-map-viewer/app/routes/geocoding.js b/map/app/routes/geocoding.js similarity index 100% rename from nocodb-map-viewer/app/routes/geocoding.js rename to map/app/routes/geocoding.js diff --git a/nocodb-map-viewer/app/server.js b/map/app/server.js similarity index 64% rename from nocodb-map-viewer/app/server.js rename to map/app/server.js index adc7b70..e571a1e 100644 --- a/nocodb-map-viewer/app/server.js +++ b/map/app/server.js @@ -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,22 @@ 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: true, // Enable for HTTPS + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000, // 24 hours + sameSite: 'lax', + domain: process.env.COOKIE_DOMAIN || undefined // Add domain support + }, + name: 'nocodb-map-session' +})); + // Security middleware app.use(helmet({ contentSecurityPolicy: { @@ -125,30 +162,242 @@ app.use(helmet({ // CORS configuration app.use(cors({ - origin: process.env.ALLOWED_ORIGINS?.split(',') || '*', - credentials: true + origin: function(origin, callback) { + // Allow requests with no origin (like mobile apps or curl requests) + if (!origin) return callback(null, true); + + const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || []; + if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'] })); -// Rate limiting +// Trust proxy for Cloudflare +app.set('trust proxy', true); + +// Rate limiting with Cloudflare support const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100 // limit each IP to 100 requests per windowMs + windowMs: 15 * 60 * 1000, + max: 100, + keyGenerator: (req) => { + // Use CF-Connecting-IP header if available (Cloudflare) + return req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip; + }, + standardHeaders: true, + legacyHeaders: false, }); const strictLimiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 20 // limit location creation to 20 per 15 minutes + max: 20, + keyGenerator: (req) => { + return req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip; + } +}); + +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: process.env.NODE_ENV === 'production' ? 10 : 50, // Increase limit slightly + message: 'Too many login attempts, please try again later.', + keyGenerator: (req) => { + return req.headers['cf-connecting-ip'] || + req.headers['x-forwarded-for']?.split(',')[0] || + req.ip; + }, + standardHeaders: true, + legacyHeaders: false, }); // 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 { + // Log request details for debugging + logger.info('Login attempt:', { + email: req.body.email, + ip: req.ip, + cfIp: req.headers['cf-connecting-ip'], + forwardedFor: req.headers['x-forwarded-for'], + userAgent: req.headers['user-agent'] + }); + + 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; + + // Force session save before sending response + req.session.save((err) => { + if (err) { + logger.error('Session save error:', err); + return res.status(500).json({ + success: false, + error: 'Session error. Please try again.' + }); + } + + 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' + }); + }); +}); + +// Add this after the /api/auth/check route +app.get('/api/debug/session', (req, res) => { + res.json({ + sessionID: req.sessionID, + session: req.session, + cookies: req.cookies, + authenticated: req.session?.authenticated || false + }); +}); + +// 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 +406,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 +429,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 +635,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 +684,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 +720,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 +756,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()} ║ ╚════════════════════════════════════════╝ `); diff --git a/nocodb-map-viewer/app/services/geocoding.js b/map/app/services/geocoding.js similarity index 100% rename from nocodb-map-viewer/app/services/geocoding.js rename to map/app/services/geocoding.js diff --git a/nocodb-map-viewer/docker-compose.yml b/map/docker-compose.yml similarity index 100% rename from nocodb-map-viewer/docker-compose.yml rename to map/docker-compose.yml