diff --git a/map/app/public/login.html b/map/app/public/login.html
index 88d9daa..69c5ba5 100644
--- a/map/app/public/login.html
+++ b/map/app/public/login.html
@@ -163,6 +163,18 @@
>
+
+
+
+
+
@@ -180,6 +192,7 @@
e.preventDefault();
const email = document.getElementById('email').value;
+ const password = document.getElementById('password').value;
const button = document.getElementById('login-button');
const errorMessage = document.getElementById('error-message');
const successMessage = document.getElementById('success-message');
@@ -198,7 +211,7 @@
headers: {
'Content-Type': 'application/json',
},
- body: JSON.stringify({ email }),
+ body: JSON.stringify({ email, password }),
credentials: 'include'
});
diff --git a/map/app/server.js b/map/app/server.js
index db1c6bd..0c4e7a1 100644
--- a/map/app/server.js
+++ b/map/app/server.js
@@ -311,12 +311,12 @@ app.post('/api/auth/login', authLimiter, async (req, res) => {
userAgent: req.headers['user-agent']
});
- const { email } = req.body;
+ const { email, password } = req.body;
- if (!email) {
+ if (!email || !password) {
return res.status(400).json({
success: false,
- error: 'Email is required'
+ error: 'Email and password are required'
});
}
@@ -338,7 +338,7 @@ app.post('/api/auth/login', authLimiter, async (req, res) => {
});
}
- // Fetch authorized emails from NocoDB
+ // Fetch user 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}`);
@@ -350,54 +350,79 @@ app.post('/api/auth/login', authLimiter, async (req, res) => {
'Content-Type': 'application/json'
},
params: {
- limit: 1000 // Adjust if you have more authorized users
+ where: `(Email,eq,${email})`,
+ limit: 1
}
});
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 including admin status
- req.session.authenticated = true;
- req.session.userEmail = email;
- req.session.userName = authorizedUser.Name || email;
- req.session.isAdmin = authorizedUser.Admin === true || authorizedUser.Admin === 1;
-
- // 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}, Admin: ${req.session.isAdmin}`);
-
- res.json({
- success: true,
- message: 'Login successful',
- user: {
- email: email,
- name: req.session.userName,
- isAdmin: req.session.isAdmin
- }
- });
- });
- } else {
- logger.warn(`Authentication failed for email: ${email}`);
- res.status(401).json({
+ if (users.length === 0) {
+ logger.warn(`No user found with email: ${email}`);
+ return res.status(401).json({
success: false,
- error: 'Email not authorized. Please contact an administrator.'
+ error: 'Invalid email or password'
});
}
+ const user = users[0];
+
+ // Check password (plain text comparison for now)
+ if (user.Password !== password && user.password !== password) {
+ logger.warn(`Invalid password for email: ${email}`);
+ return res.status(401).json({
+ success: false,
+ error: 'Invalid email or password'
+ });
+ }
+
+ // Update last login time
+ try {
+ const updateUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}/${user.Id || user.id || user.ID}`;
+ await axios.patch(updateUrl, {
+ 'Last Login': new Date().toISOString(),
+ last_login: new Date().toISOString()
+ }, {
+ headers: {
+ 'xc-token': process.env.NOCODB_API_TOKEN,
+ 'Content-Type': 'application/json'
+ }
+ });
+ } catch (updateError) {
+ logger.warn('Failed to update last login time:', updateError.message);
+ // Don't fail the login if we can't update last login time
+ }
+
+ // Set session including admin status
+ req.session.authenticated = true;
+ req.session.userEmail = email;
+ req.session.userName = user.Name || user.name || email;
+ req.session.isAdmin = user.Admin === true || user.Admin === 1 || user.admin === true || user.admin === 1;
+ req.session.userId = user.Id || user.id || user.ID;
+
+ // 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}, Admin: ${req.session.isAdmin}`);
+
+ res.json({
+ success: true,
+ message: 'Login successful',
+ user: {
+ email: email,
+ name: req.session.userName,
+ isAdmin: req.session.isAdmin
+ }
+ });
+ });
+
} catch (error) {
logger.error('Login error:', error.message);
res.status(500).json({
@@ -1823,6 +1848,172 @@ app.get('/api/debug/walk-sheet-raw', requireAdmin, async (req, res) => {
}
});
+// Admin user management endpoints
+app.get('/api/admin/users', requireAdmin, async (req, res) => {
+ try {
+ if (!LOGIN_SHEET_ID) {
+ return res.status(500).json({
+ success: false,
+ error: 'Login sheet not configured'
+ });
+ }
+
+ const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`;
+
+ const response = await axios.get(url, {
+ headers: {
+ 'xc-token': process.env.NOCODB_API_TOKEN,
+ 'Content-Type': 'application/json'
+ },
+ params: {
+ limit: 100,
+ sort: '-created_at'
+ }
+ });
+
+ const users = response.data.list || [];
+
+ // Remove password field from response for security
+ const safeUsers = users.map(user => {
+ const { Password, password, ...safeUser } = user;
+ return safeUser;
+ });
+
+ res.json({
+ success: true,
+ users: safeUsers
+ });
+
+ } catch (error) {
+ logger.error('Error fetching users:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to fetch users'
+ });
+ }
+});
+
+app.post('/api/admin/users', requireAdmin, async (req, res) => {
+ try {
+ const { email, password, name, admin } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({
+ success: false,
+ error: 'Email and password are required'
+ });
+ }
+
+ if (!LOGIN_SHEET_ID) {
+ return res.status(500).json({
+ success: false,
+ error: 'Login sheet not configured'
+ });
+ }
+
+ // Check if user already exists
+ const checkUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`;
+
+ const checkResponse = await axios.get(checkUrl, {
+ headers: {
+ 'xc-token': process.env.NOCODB_API_TOKEN,
+ 'Content-Type': 'application/json'
+ },
+ params: {
+ where: `(Email,eq,${email})`,
+ limit: 1
+ }
+ });
+
+ if (checkResponse.data.list && checkResponse.data.list.length > 0) {
+ return res.status(400).json({
+ success: false,
+ error: 'User with this email already exists'
+ });
+ }
+
+ // Create new user
+ const userData = {
+ Email: email,
+ email: email,
+ Password: password,
+ password: password,
+ Name: name || '',
+ name: name || '',
+ Admin: admin === true,
+ admin: admin === true,
+ 'Created At': new Date().toISOString(),
+ created_at: new Date().toISOString()
+ };
+
+ const response = await axios.post(checkUrl, userData, {
+ headers: {
+ 'xc-token': process.env.NOCODB_API_TOKEN,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ res.status(201).json({
+ success: true,
+ message: 'User created successfully',
+ user: {
+ id: response.data.Id || response.data.id || response.data.ID,
+ email: email,
+ name: name,
+ admin: admin
+ }
+ });
+
+ } catch (error) {
+ logger.error('Error creating user:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to create user'
+ });
+ }
+});
+
+app.delete('/api/admin/users/:id', requireAdmin, async (req, res) => {
+ try {
+ const userId = req.params.id;
+
+ if (!LOGIN_SHEET_ID) {
+ return res.status(500).json({
+ success: false,
+ error: 'Login sheet not configured'
+ });
+ }
+
+ // Don't allow admins to delete themselves
+ if (userId === req.session.userId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Cannot delete your own account'
+ });
+ }
+
+ const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}/${userId}`;
+
+ await axios.delete(url, {
+ headers: {
+ 'xc-token': process.env.NOCODB_API_TOKEN
+ }
+ });
+
+ res.json({
+ success: true,
+ message: 'User deleted successfully'
+ });
+
+ } catch (error) {
+ logger.error('Error deleting user:', error);
+ res.status(500).json({
+ success: false,
+ error: 'Failed to delete user'
+ });
+ }
+});
+
// Error handling middleware
app.use((err, req, res, next) => {
logger.error('Unhandled error:', err);
diff --git a/map/build-nocodb.sh b/map/build-nocodb.sh
index 24180fe..e96d37b 100755
--- a/map/build-nocodb.sh
+++ b/map/build-nocodb.sh
@@ -411,6 +411,12 @@ create_login_table() {
"uidt": "Email",
"rqd": true
},
+ {
+ "column_name": "password",
+ "title": "Password",
+ "uidt": "SingleLineText",
+ "rqd": true
+ },
{
"column_name": "name",
"title": "Name",
@@ -563,9 +569,10 @@ create_default_admin() {
local login_table_id=$2
print_status "Creating default admin user..."
-
+
local admin_data='{
"email": "admin@thebunkerops.ca",
+ "password": "admin123",
"name": "Administrator",
"admin": true,
"created_at": "'"$(date -u +"%Y-%m-%d %H:%M:%S")"'"
@@ -663,8 +670,15 @@ main() {
print_status " - NOCODB_VIEW_URL (for locations table)"
print_status " - NOCODB_LOGIN_SHEET (for login table)"
print_status " - NOCODB_SETTINGS_SHEET (for settings table)"
- print_status "4. The default admin user is: admin@thebunkerops.ca"
- print_status "5. Start adding your location data!"
+ print_status "4. The default admin user is: admin@thebunkerops.ca with password: admin123"
+ print_status "5. IMPORTANT: Change the default password after first login!"
+ print_status "6. Start adding your location data!"
+
+ print_warning ""
+ print_warning "IMPORTANT: This script created a NEW base. Your existing data was NOT modified."
+ print_warning "Please update your .env file with the new table URLs from the newly created base."
+ print_warning "SECURITY: Change the default admin password immediately after first login!"
+
print_warning ""
print_warning "IMPORTANT: This script created a NEW base. Your existing data was NOT modified."