Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f3aef3a
Fixes to remediate Issue 2
Zachary-Squires Nov 11, 2025
204f0ef
Update docker-compose.yml
Zachary-Squires Nov 11, 2025
759870d
Update docker-compose.yml to put comments above line
Zachary-Squires Nov 17, 2025
a817ee5
Update docker-compose.yml to remove double empty line with spaces at …
Zachary-Squires Nov 17, 2025
4c8354f
Implement PR #1554 Suggestions
Zachary-Squires Nov 18, 2025
fa7baf5
Removing extra line at the end of docker-compose.yml
Zachary-Squires Nov 18, 2025
52815d0
Changes to address feedback on PR #1554
Zachary-Squires Dec 3, 2025
805e369
Merge branch 'OpenEnergyDashboard:development' into development
Zachary-Squires Jan 12, 2026
1d3bcc6
Changes to Address Feedback
Zachary-Squires Jan 12, 2026
0d5fa73
Merge branch 'OpenEnergyDashboard:development' into development
Zachary-Squires Jan 19, 2026
05b62c4
Changes to address feedback
Zachary-Squires Jan 19, 2026
5a002c8
Restore executable bit
Zachary-Squires Jan 19, 2026
b958b00
Changes to address most recent feedback on PR #1554
Zachary-Squires Feb 2, 2026
1d8c650
Changes to address comments on PR #1554 from Feb 4th
Zachary-Squires Feb 23, 2026
90e5140
Merge branch 'OpenEnergyDashboard:development' into development
Zachary-Squires Feb 23, 2026
3fbc0c2
Changes to address feedback, no restart on install
Zachary-Squires Apr 6, 2026
0211c28
Typo
Zachary-Squires Apr 6, 2026
f11c826
Fix for accidentally setting passwords to the defaults
Zachary-Squires Apr 6, 2026
ab538b9
Merge branch 'development' into development
Zachary-Squires Apr 8, 2026
a8c12ac
Revert commit from OED in docker-compose.yml that makes it crash
Zachary-Squires Apr 12, 2026
99ce3d0
Changes to address 4/9 feedback on #1554
Zachary-Squires Apr 13, 2026
0199d6b
Adding a confirmation when manually changing the password
Zachary-Squires Apr 18, 2026
24e2fb3
Revert "Merge branch 'development' into development"
Zachary-Squires Apr 20, 2026
43d4ef8
Adding support for infisical password vault
Zachary-Squires Apr 20, 2026
b5fd5e0
Reverting README.md change
Zachary-Squires Apr 20, 2026
394c93d
Revert "Revert "Merge branch 'development' into development""
Zachary-Squires Apr 20, 2026
54b3791
Changes to address 4/21 feedback on PR #1609
Zachary-Squires Apr 22, 2026
8439239
Merge branch 'OpenEnergyDashboard:development' into password-vault
Zachary-Squires Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ services:
environment:
# Custom PGDATA per recommendations from official Docker page
- PGDATA=/var/lib/postgresql/data/pgdata
- POSTGRES_PASSWORD=pleaseChange # default postgres password that should be changed for security.
# The postgres database will only pull from this value on the first
# startup of OED. If a change is desired after this, one must
# run the following command in the docker web terminal:
# "npm run changePostgresPassword -- postgrespassword oedpassword"
# Replace "postgrespassword" with your desired postgres user password
# and "oedpassword" with your desired oed user password.
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-pleaseChange}
volumes:
- ./postgres-data:/var/lib/postgresql/data/pgdata
healthcheck:
Expand All @@ -29,15 +35,15 @@ services:
web:
# Configuration variables for the app.
environment:
- OED_PRODUCTION=no
- OED_PRODUCTION=no # Set this value to yes or no, other values will result in a configuration error
- OED_SERVER_PORT=3000
- OED_DB_USER=oed
- OED_DB_DATABASE=oed
- OED_DB_TEST_DATABASE=oed_testing
- OED_DB_PASSWORD=opened
- OED_DB_PASSWORD=${OED_DB_PASSWORD:-opened} # See comment regarding POSTGRES_PASSWORD above, same situation
- OED_DB_HOST=database # Docker will set this hostname
- OED_DB_PORT=5432
- OED_TOKEN_SECRET=?
- OED_TOKEN_SECRET=${OED_TOKEN_SECRET:-?} #Automatically generated when OED is run in production
- OED_LOG_FILE=log.txt
- OED_MAIL_METHOD=none # Method of sending mail. Supports "secure-smtp", "none". Case insensitive.
- OED_MAIL_SMTP=smtp.example.com # Edit this
Expand All @@ -47,13 +53,15 @@ services:
- OED_MAIL_FROM=mydomain@example.com # The email address that the email will come from
- OED_MAIL_TO=someone@example.com # Set the destination address here for where to send emails
- OED_MAIL_ORG=My Organization Name # Org name for mail that is included in the subject
- PASSWORD_VAULT=no # Set to yes to load DB passwords from Infisical
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the design doc is in place for OED, it might be nice to put a link to it here so people can easily find the directions for enabling the vault.

# Changing this value does not impact what OED displays.
# What it will change is the date/time stamp on logs, notes and change dates that place the current date/time.
# It can also impact the interpretation of readings sent to OED such as Unix timestamps.
- TZ=Etc/UTC # Set the timezone of the Docker container where OED runs the web services.
# If in a subdirectory, set it here
# - OED_SUBDIR=/subdir/
# Set the correct build environment.
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-pleaseChange}
build:
context: ./
dockerfile: ./containers/web/Dockerfile
Expand Down Expand Up @@ -111,7 +119,3 @@ services:
/bin/sh -c "
rm -f /tmp/.X99-lock &&
Xvfb :99 -screen 0 1024x768x16"
read_only: true
security_opt:
- no-new-privileges:true

3 changes: 2 additions & 1 deletion package.json
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"generateTestingData": "node -e 'require(\"./src/server/data/automatedTestingData\").generateTestingData()'",
"testData": "node -e 'require(\"./src/server/data/automatedTestingData.js\").insertSpecialUnitsConversionsMetersGroups()'",
"webData": "node -e 'require(\"./src/server/data/websiteData.js\").insertWebsiteData()'",
"addLogMsg": "node -e 'require(\"./src/server/services/addLogMsg.js\").addLogMsgToDB()'"
"addLogMsg": "node -e 'require(\"./src/server/services/addLogMsg.js\").addLogMsgToDB()'",
"changePostgresPassword": "node ./src/server/util/changePostgresPass.js"
},
"nodemonConfig": {
"watch": [
Expand Down
73 changes: 68 additions & 5 deletions src/scripts/installOED.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ if [ -f ".env" ]; then
source .env
fi

# Creating a centralized variable to keep track of the type of installation.
INSTALL_MODE="production"

if [ "$production" = "yes" ] || [ "$OED_PRODUCTION" = "yes" ]; then
INSTALL_MODE="production"
elif [ "$production" = "no" ] || [ "$OED_PRODUCTION" = "no" ]; then
INSTALL_MODE="development"
else
printf "\nFailure: Invalid or missing environment configuration."
printf "\nSet OED_PRODUCTION to 'yes' for production or 'no' for development."
exit 10
fi

# Skip the install if the node_modules were installed before the package files.
# The two package files
packageFile="package.json"
Expand Down Expand Up @@ -147,7 +160,7 @@ else

# Create a user
set -e
if [ "$production" == "no" ] && [ ! "$OED_PRODUCTION" == "yes" ]; then
if [ "$INSTALL_MODE" = "development" ]; then
npm run createUser -- $usernameTest password
createuserTest_code=$?
# this second username uses an email: test@example.com and we will remove this eventually
Expand Down Expand Up @@ -176,7 +189,7 @@ else
fi

# Build webpack if needed
if [ "$production" == "yes" ] || [ "$OED_PRODUCTION" == "yes" ]; then
if [ "$INSTALL_MODE" = "production" ]; then
npm run webpack:build
elif [ "$dostart" == "no" ]; then
npm run webpack
Expand All @@ -186,13 +199,63 @@ printf "%s\n" "OED install finished"

# Start OED
if [ "$dostart" == "yes" ]; then
if [ "$production" == "yes" ] || [ "$OED_PRODUCTION" == "yes" ]; then
if [ "$INSTALL_MODE" = "production" ]; then
printf "%s\n" "Starting OED in production mode"
# Checking if the user has set a mail method and left one of the mailing environment variables default, warning if so
if [ -z "$OED_MAIL_METHOD" ] || [ "$OED_MAIL_METHOD" != "none" ]; then
if [ "$OED_MAIL_SMTP" = "smtp.example.com" ] || \
[ "$OED_MAIL_SMTP_PORT" = "465" ] || \
[ "$OED_MAIL_IDENT" = "someone@example.com" ] || \
[ "$OED_MAIL_CREDENTIAL" = "credential" ] || \
[ "$OED_MAIL_FROM" = "mydomain@example.com" ] || \
[ "$OED_MAIL_TO" = "someone@example.com" ] || \
[ "$OED_MAIL_ORG" = "My Organization Name" ]; then
printf "\n********************************************************************************\n"
printf "* WARNING: You have set your mail method but one or more of the mail environment variables are still set to the default value!*\n"
printf "********************************************************************************\n\n"
fi
fi
# If the user is in production and their token secret has been left default, generating a random one
if [ -z "$OED_TOKEN_SECRET" ] || [ "$OED_TOKEN_SECRET" = "?" ]; then
printf "\nNo valid OED_TOKEN_SECRET detected. Generating a secure random secret...\n"

# Generate 32 bytes of random data and convert to 64-character hex
OED_TOKEN_SECRET=$(openssl rand -hex 32)
export OED_TOKEN_SECRET

printf "\n********************************************************************************\n"
printf "Generated OED_TOKEN_SECRET: %s\n" "$OED_TOKEN_SECRET"
printf "\nMake sure to save or change this value"
printf "********************************************************************************\n\n"

# Save to .env for future runs
if [ -f ".env" ]; then
if grep -q "^OED_TOKEN_SECRET=" .env; then
sed -i "s/^OED_TOKEN_SECRET=.*/OED_TOKEN_SECRET=$OED_TOKEN_SECRET/" .env
else
echo "OED_TOKEN_SECRET=$OED_TOKEN_SECRET" >> .env
fi
else
echo "OED_TOKEN_SECRET=$OED_TOKEN_SECRET" > .env
fi
fi
# If the user is in production and their postgres password has been left default, generating a random one
if [ -z "$POSTGRES_PASSWORD" ] || [ "$POSTGRES_PASSWORD" = "pleaseChange" ]; then
printf "\nNo valid PostgreSQL password detected. Generating a secure random password...\n"
node ./src/server/util/changePostgresPass.js "" "" install
fi
npm run start
else
printf "%s\n" "Starting OED in development mode"
# Warning the user if they've left their token or postgres password default, we don't randomly generate it in dev mode
if [ -z "$OED_TOKEN_SECRET" ] || [ "$OED_TOKEN_SECRET" = "?" ]; then
printf "Warning: you are using OED in development mode with the default OED_TOKEN_SECRET set in docker-compose.yml. If this is not intentional, please update it there.\n"
fi
if [ -z "$POSTGRES_PASSWORD" ] || [ "$POSTGRES_PASSWORD" = "pleaseChange" ]; then
printf "* Warning: you are using OED in development mode with the default PostgreSQL password set in docker-compose.yml. If this is not intentional, please update it there. *\n" printf "********************************************************************************\n\n"
fi
printf "%s\n" "Starting OED in development mode."
./src/scripts/devstart.sh
fi
else
printf "%s\n" "Not starting OED due to --nostart"
printf "%s\n" "Not starting OED due to --nostart."
fi
6 changes: 5 additions & 1 deletion src/server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
const loadInfisicalSecrets = require('./util/loadInfisicalSecrets');
// Try to load the .env file

const envPath = path.join(__dirname, '..', '..', '.env');
try {
fs.accessSync(envPath);
dotenv.config({ path: envPath });
dotenv.config({ path: envPath, override: true });
} catch (err) {
// TODO: Check if valid env variables are actually loaded despite the lack of a file, only log if they are not
// console.log("Couldn't load a .env file");
}

// Optional Infisical password vault support. If PASSWORD_VAULT is enabled, override the
// database password environment variables from Infisical secrets
loadInfisicalSecrets();

const config = {};

Expand Down
202 changes: 202 additions & 0 deletions src/server/util/changePostgresPass.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { Client } = require('pg');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const dotenv = require('dotenv');

const ROOT_ENV_PATH = path.resolve(__dirname, '..', '..', '..', '.env');
const CWD_ENV_PATH = path.resolve(process.cwd(), '.env');
const ENV_PATH = fs.existsSync(ROOT_ENV_PATH) ? ROOT_ENV_PATH : CWD_ENV_PATH;

// Load whichever .env file is available and override existing process vars
// so repeated invocations in one session can pick up the newest credentials.
if (fs.existsSync(ENV_PATH)) {
dotenv.config({ path: ENV_PATH, override: true });
}

// Parse the shared .env file directly so we can prefer the most recent values
// when npm or Docker still has a stale environment variable.
function parseEnvFile(envPath) {
if (!fs.existsSync(envPath)) {
return {};
}

return dotenv.parse(fs.readFileSync(envPath, 'utf8'));
}

// Generate a secure random password
function generatePassword() {
return crypto.randomBytes(32).toString('base64');
}

// Escape single quotes in password for SQL
function escapePassword(password) {
return password.replace(/'/g, "''");
}

// Prompt the user for an explicit yes/no confirmation
function promptConfirmation(promptText) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

return new Promise((resolve) => {
rl.question(`${promptText} `, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === 'yes');
});
});
}

// Update .env with PostgreSQL and OED user passwords
function updateEnvFile(postgresPassword, oedPassword) {
let env = '';

// Reading current .env contents if it exists
if (fs.existsSync(ENV_PATH)) {
env = fs.readFileSync(ENV_PATH, 'utf8');
}

// Updating the postgres password if it exists, else appending it instead
if (/^POSTGRES_PASSWORD=/m.test(env)) {
env = env.replace(/^POSTGRES_PASSWORD=.*/m, `POSTGRES_PASSWORD=${postgresPassword}`);
} else {
env = env.trimEnd() + `\nPOSTGRES_PASSWORD=${postgresPassword}\n`;
}

// Updating the oed user password if it exists, else appending it instead
if (/^OED_DB_PASSWORD=/m.test(env)) {
env = env.replace(/^OED_DB_PASSWORD=.*/m, `OED_DB_PASSWORD=${oedPassword}`);
} else {
env = env.trimEnd() + `\nOED_DB_PASSWORD=${oedPassword}\n`;
}

// Writing new passwords to .env or creating it if it doesn't exist yet, only allowing the current user to read and write
fs.writeFileSync(ENV_PATH, env, { mode: 0o600 });
console.log('.env updated with new PostgreSQL and OED passwords');
}

// Pause execution
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// Detect retryable Postgres errors:
// 'tuple concurrently updated': row was modified by another transaction
// 40001 (serialization_failure): concurrent transaction conflict
// 55P03 (lock_not_available): couldn't acquire lock, may be freed soon
function shouldRetryError(error) {
return (
error.message.includes('tuple concurrently updated') ||
(error.code && (error.code === '40001' || error.code === '55P03'))
);
}

// Change the database passwords for both the default postgres user and the OED user
// if this is done after the initial setup, OED must be restarted to get a connection with the server
async function changePasswords() {
const fileEnv = parseEnvFile(ENV_PATH);

// Determine context: 'install' (automatic, no restart needed) or 'manual' (script, restart needed)
const isManual = process.argv[4] !== 'install';

// Warn if manual invocation that all OED users will lose access until restart
if (isManual) {
console.error('');
console.error('WARNING: This will change database passwords immediately.');
console.error('All currently logged-in users will experience disconnections.');
console.error('OED will not work for anyone until the server is restarted.');
console.error('');

const confirmed = await promptConfirmation('Do you want to continue? Type yes to proceed:');
if (!confirmed) {
console.error('Aborting password change. No changes were made.');
process.exit(0);
}
}

// Prefer the most recent passwords from the .env file over process.env which may be outdated
if (fileEnv.POSTGRES_PASSWORD) {
process.env.POSTGRES_PASSWORD = fileEnv.POSTGRES_PASSWORD;
}
if (fileEnv.OED_DB_PASSWORD) {
process.env.OED_DB_PASSWORD = fileEnv.OED_DB_PASSWORD;
}

// If arguments are included, treat them as the new passwords
const currentPostgresPassword = fileEnv.POSTGRES_PASSWORD || process.env.POSTGRES_PASSWORD || 'pleaseChange';
const newPostgresPassword = process.argv[2] || generatePassword();
const newOedPassword = process.argv[3] || generatePassword();

const clientConfig = {
host: process.env.OED_DB_HOST || 'database',
port: parseInt(process.env.OED_DB_PORT || '5432', 10),
user: 'postgres',
password: currentPostgresPassword,
database: 'postgres',
connectionTimeoutMillis: 10000
};

// Try the password update multiple times to handle transient lock error
for (let attempt = 1; attempt <= 3; attempt += 1) {
const client = new Client(clientConfig);

try {
await client.connect();

const sqlPostgres = `ALTER USER postgres WITH PASSWORD '${escapePassword(newPostgresPassword)}'`;
await client.query(sqlPostgres);

const sqlOED = `ALTER USER oed WITH PASSWORD '${escapePassword(newOedPassword)}'`;
await client.query(sqlOED);

await client.end();

updateEnvFile(newPostgresPassword, newOedPassword);

console.log('********************************************************************************');
console.log('Generated a secure PostgreSQL and OED password and applied them successfully.');
console.log('The passwords have been stored in ".env" for reference.');
if (isManual) {
console.log('');
console.log('CRITICAL: OED is now disconnected for all users.');
console.log('You must restart OED immediately for it to function.');
console.log('All active user sessions will be terminated.');
} else {
console.log('(Installation mode: restart not required)');
}
console.log('********************************************************************************\n');

process.exit(0);
} catch (error) {
await client.end().catch(() => {});

if (shouldRetryError(error) && attempt < 3) {
console.error(`Transient Postgres error on attempt ${attempt}: ${error.message}`);
console.error('Retrying password update...');
await sleep(1000 * attempt);
continue;
}

console.error('Error changing PostgreSQL or OED password:', error.message);

if (error.message.includes('password authentication')) {
console.error('Authentication failed: default password may already be changed or password used is incorrect.');
}

process.exit(1);
}
}
}

if (require.main === module) {
changePasswords();
}

module.exports = { changePasswords };
Loading
Loading